In [ ]:
 

Abschlußprojekt für Data Analyst: Entwicklung eines Autopreisvorhersagemodells und Integration in ein Dashboard zur Anwendung.¶

Autor Guangfa Wu, 18.08.2025, Stuttgart¶

Daten: https://www.kaggle.com/datasets/hellbuoy/car-price-prediction¶

Projektidee, Beschreibung:¶

Problembeschreibung: Autopreis vorhersagen¶

Auto_models.jpg

Ein chinesisches Automobilunternehmen, Geely Auto, plant den Eintritt in den US-amerikanischen Markt, indem es dort eine Produktionsstätte errichtet und Autos lokal herstellt, um mit amerikanischen und europäischen Konkurrenten zu konkurrieren. Dazu hat Geely ein Beratungsunternehmen für die Automobilbranche beauftragt, um die Faktoren zu analysieren, die den Preis von Autos beeinflussen. Insbesondere möchten sie verstehen, welche Faktoren den Preis von Autos auf dem amerikanischen Markt bestimmen, da diese sich deutlich vom chinesischen Markt unterscheiden könnten.

Das Unternehmen möchte wissen:¶

  • Welche Variablen sind signifikant für die Vorhersage des Autopreises?
  • Wie gut erklären diese Variablen den Preis eines Autos?

Vorgegensweise:¶

CRISP-DM wird angewendet¶

Basierend auf verschiedenen Marktforschungen hat das Beratungsunternehmen einen umfangreichen Datensatz mit unterschiedlichen Fahrzeugtypen auf dem US-amerikanischen Markt zusammengetragen. Lösung der Aufgabestellung:

  • Ein Modell soll entwickelt werden, das den Preis von Autos basierend auf den verfügbaren unabhängigen Variablen am besten vorhersagt. Da ein Auto aus verschieden Komponenten besteht, ist es nur sinnvoll, alle Komponenten zusammen zu betrachten und ein Preismodell zu erstellen.
  • Gleichzeitig kann auch untersucht werden welche einzelnen Faktoren den Preis beeinflussen.
  • Es wird ein Dashboard für Modelltraining durch Auswahl von beteiligten Spaten erstellt. Der Anwender kann selbst die Spalten auswählen und das Modell trinieren und das Ergebnis anzeigen und bewerten.

Anwendung:¶

Das Modell soll dem Management dabei helfen zu verstehen:¶

  • Wie sich der Preis je nach Veränderung einzelner Merkmale verändert
  • Welche Stellschrauben (Design, Strategie etc.) genutzt werden können, um bestimmte Preisniveaus zu erreichen.
  • Zusätzlich ist das Modell ein hilfreiches Werkzeug, um die Preisdynamik in einem neuen Markt wie den USA zu verstehen.
  • Es wird ein Dashboard erstellt. der Anwender kann selbst die einzelnen Parameter auswählen und den Preis abzuschätzen

Der bereitgestellte Datensatz dient ausschließlich zu Lernzwecken. Ziehen keine Rückschlüsse auf reale Märkte oder tatsächliche Marktbedingungen.¶

Die Arbeitsschritte richten sich nach CRISP-DM¶

CRISP-DM_Process_Diagram.png

1. Business Understanding (Aufgabendefinition)¶

Fahrzeugsegmente und Preisspannen¶

Das Preissegment für Autos wird üblicherweise in verschiedene Kategorien eingeteilt, die sich an der Größe, Ausstattung und Komfort des Fahrzeugs orientieren.
Oft werden diese Segmente mit Buchstaben (A, B, C, D, …) bezeichnet und unterscheiden sich durch typische Preisspannen.

Hinweis: Die Preisbereiche sind Richtwerte und können je nach Marke, Modell, Ausstattung und Region variieren.


🚗 Übersicht gängiger Fahrzeugsegmente¶

Fahrzeugsegment Beschreibung Typische Preisspanne (Neuwagen) Bemerkungen zu Kosten
A-Segment Kleinstwagen, sehr kompakt, ideal für Stadtverkehr unter 15.000 € Günstig in Anschaffung und Unterhalt
B-Segment Kleinwagen, etwas größer, alltagstauglicher, mehr Komfort 15.000 – 25.000 € Kompromiss zwischen Preis und Platz/Komfort
C-Segment Kompaktklasse, familienfreundlich, gute Mischung aus Platz und Ausstattung 20.000 – 35.000 € Vielseitig, gute Ausstattung
D-Segment Mittelklasse, komfortabler, bessere Ausstattung ab ca. 35.000 € Höhere Anschaffungskosten
Oberklasse / Luxus Große und luxuriöse Fahrzeuge ab ca. 50.000 € Teuer, höherer Unterhalt, Luxusausstattung

📌 Relevanz für die Analyse¶

In der Aufgabenstellung gibt der Hersteller keine Informationen, auf welches Preissegment er sich fokussieren möchte.
Als Data Analyst ist es daher notwendig:

  • Mit dem Auftraggeber zu kommunizieren, um die Anforderungen und Geschäftsziele genau zu verstehen.
  • Klarzustellen, ob die Vorhersage für den Gesamtmarkt oder für ein bestimmtes Preissegment erfolgen soll.
  • Auf Basis der gewählten Segmente die Preisvorhersagen gezielt an die jeweilige Kategorie anzupassen.

Datenquelle:¶

CarPrice_Assignment.csv¶

Information von Auto: 26 Spalten¶

  • car_ID: Identification Number for Each Car
  • symbling: Car's Safety Rating (-3 (sehr sicher) bis +3 (hohes Risiko).
  • CarName: Name of the Car Model
  • fueltype: Type of Fuel Used (Gasoline, Diesel, Electric, etc.)
  • aspiration: Type of Aspiration (Standard or Turbocharged)
  • doornumber: Number of Doors on the Car
  • carbody: Style of the Car's Body (Sedan, Coupe, SUV, etc.)
  • drivewheel: Type of Drive Wheels (Front, Rear, All)
  • enginelocation: Location of the Car's Engine (Front or Rear)
  • wheelbase: Length of the Car's Wheelbase
  • carlength: Overall Length of the Car
  • carwidth: Width of the Car
  • carheight: Height of the Car
  • curbweight: Weight of the Car without Passengers or Cargo
  • enginetype: Type of Engine (Gas, Diesel, Electric, etc.)
  • cylindernumber: Number of Cylinders in the Engine
  • enginesize: Size of the Car's Engine
  • fuelsystem: Type of Fuel Delivery System
  • boreratio: Bore-to-Stroke Ratio of the Engine
  • stroke: Stroke Length of the Engine
  • compressionratio: Compression Ratio of the Engine
  • horsepower: Car's Engine Horsepower
  • peakrpm: Engine's Peak RPM (Revolutions Per Minute)
  • citympg: Miles Per Gallon (MPG) in City Driving
  • highwaympg: MPG on the Highway
  • price: Price of the Car
In [45]:
# import packages

import pandas as pd
import numpy as np
import json
import math
import matplotlib
from datetime import timedelta, datetime
import matplotlib.pyplot as plt
import matplotlib.mlab as mlab
from matplotlib.pyplot import figure
from datetime import date

import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go


plt.style.use('seaborn-v0_8-notebook')
%matplotlib inline
matplotlib.rcParams['figure.figsize'] = (12,8)
pd.options.mode.chained_assignment = None
import seaborn as sns

Einlesenvon Daten aus der csv-Datei:¶

In [49]:
# Einlesen von Daten
df = pd.read_csv("CarPrice_Assignment.csv")
door_mapCt=0     # um mehrfach map zu vermeiden
door_mapCt1=0  # um mehrfach map zu vermeiden
pd.set_option('display.max_columns', None) # zeigt alle Spalten an

2. Data Understanding (Auswahl der relevanten Datenbestände)¶

2.1 Deskriptive Analyse¶

Bei der deskriptiven Analyse werden die Daten mithilfe einfacher statistischer Methoden untersucht, um einen Überblick über die Datenstruktur, Verteilungen und Zusammenhänge zu erhalten.

Das Ziel ist es, die Eigenschaften der Daten zu beschreiben, z. B.:

  • Mittelwerte
  • Streuung (Standardabweichung, Varianz)
  • Häufigkeiten
  • Verteilungen

👉 Wichtig: Es geht nicht um Vorhersagen oder Ursache-Wirkungs-Beziehungen, sondern um ein erstes Verständnis der Daten.


🔎 Erste Methoden für einen Überblick¶

  • df.head() → zeigt die ersten Zeilen der Daten
  • df.info() → Überblick über Datentypen und fehlende Werte
  • df.describe() → zentrale statistische Kennzahlen (Mittelwert, Min, Max, Quartile)
  • df.nunique() → Anzahl der eindeutigen Werte pro Spalte
  • Wertverteilungen mit value_counts() für kategoriale Spalten

Diese Methoden liefern schnell ein klares Bild über die Struktur und Qualität der Daten.

Methode Zweck / Was sie zeigt Warum sinnvoll für Data Analysts
df.info() Gibt Spaltennamen, Datentypen, Anzahl nicht fehlender Werte und Speicherverbrauch aus Schneller Überblick über Datentypen, fehlende Werte und Speicherbedarf. Hilft bei der Datenvorbereitung.
df.describe() Statistische Kennzahlen numerischer Spalten: count, mean, std, min, 25%, 50%, 75%, max Erkennt Verteilungen, Ausreißer und zentrale Tendenzen. Grundlage für Transformationen und Feature Engineering.
df.nunique() Anzahl der eindeutigen Werte pro Spalte Identifikation von kategorialen Spalten, Variabilität von Features, Erkennen von konstanten oder redundanten Spalten.
df.isna().sum() Anzahl der fehlenden Werte (NaN) pro Spalte Hilft, Spalten mit fehlenden Daten schnell zu identifizieren, Planung von Imputation oder Bereinigung der Daten.
In [50]:
# Ein Überblick über die Daten mit df.head()
df.head()
Out[50]:
car_ID symboling CarName fueltype aspiration doornumber carbody drivewheel enginelocation wheelbase carlength carwidth carheight curbweight enginetype cylindernumber enginesize fuelsystem boreratio stroke compressionratio horsepower peakrpm citympg highwaympg price
0 1 3 alfa-romero giulia gas std two convertible rwd front 88.6 168.8 64.1 48.8 2548 dohc four 130 mpfi 3.47 2.68 9.0 111 5000 21 27 13495.0
1 2 3 alfa-romero stelvio gas std two convertible rwd front 88.6 168.8 64.1 48.8 2548 dohc four 130 mpfi 3.47 2.68 9.0 111 5000 21 27 16500.0
2 3 1 alfa-romero Quadrifoglio gas std two hatchback rwd front 94.5 171.2 65.5 52.4 2823 ohcv six 152 mpfi 2.68 3.47 9.0 154 5000 19 26 16500.0
3 4 2 audi 100 ls gas std four sedan fwd front 99.8 176.6 66.2 54.3 2337 ohc four 109 mpfi 3.19 3.40 10.0 102 5500 24 30 13950.0
4 5 2 audi 100ls gas std four sedan 4wd front 99.4 176.6 66.4 54.3 2824 ohc five 136 mpfi 3.19 3.40 8.0 115 5500 18 22 17450.0

2.1.1 Die Methode df.info() in Pandas¶

Die Methode df.info() gibt einen kompakten Überblick über den DataFrame.
Sie zeigt für jede Spalte:

  • Spaltenname
  • Anzahl der nicht fehlenden Werte (Non-Null Count)
  • Datentyp (dtype)
  • Speicherverbrauch des DataFrames

📌 Ergebnisse für die Projektdaten¶

  1. Es gibt 26 Spalten und 205 Zeilen.
  2. Es gibt keine fehlenden Werte (NaN).
  3. Die Spalten doornumber und cylindernumber werden als object (Text) dargestellt.
    • Dies sollte untersucht werden, da sie eigentlich numerische Werte (Anzahl der Türen bzw. Zylinder) enthalten sollten.
    • Möglicher Grund: Werte könnten als Strings gespeichert sein (z. B. "four" statt 4).
In [47]:
# zeige die kompakte Information über die Daten
df.info()
<class 'pandas.core.frame.DataFrame'>
Index: 203 entries, 0 to 204
Data columns (total 28 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   car_ID            203 non-null    int64  
 1   symboling         203 non-null    int64  
 2   CarName           203 non-null    object 
 3   fueltype          203 non-null    object 
 4   aspiration        203 non-null    object 
 5   doornumber        203 non-null    int64  
 6   carbody           203 non-null    object 
 7   drivewheel        203 non-null    object 
 8   enginelocation    203 non-null    object 
 9   wheelbase         203 non-null    float64
 10  carlength         203 non-null    float64
 11  carwidth          203 non-null    float64
 12  carheight         203 non-null    float64
 13  curbweight        203 non-null    int64  
 14  enginetype        203 non-null    object 
 15  cylindernumber    203 non-null    int64  
 16  enginesize        203 non-null    int64  
 17  fuelsystem        203 non-null    object 
 18  boreratio         203 non-null    float64
 19  stroke            203 non-null    float64
 20  compressionratio  203 non-null    float64
 21  horsepower        203 non-null    int64  
 22  peakrpm           203 non-null    int64  
 23  citympg           203 non-null    int64  
 24  highwaympg        203 non-null    int64  
 25  price             203 non-null    float64
 26  brand             203 non-null    object 
 27  model             203 non-null    object 
dtypes: float64(8), int64(10), object(10)
memory usage: 54.1+ KB

2.1.2 Check NAN: Ergebnis:keine NaN vorhanden. Die Behandlung von NaN entfällt.¶

In [51]:
# Prüfen wie viele Nan jeder Spalte gibt: zwar df.info() hat gezeigt es keine NAN gibt. es ist üblich df.isna().sum() auch mal zu benutzen
# df.isnull().sum()
df.isna().sum()
Out[51]:
car_ID              0
symboling           0
CarName             0
fueltype            0
aspiration          0
doornumber          0
carbody             0
drivewheel          0
enginelocation      0
wheelbase           0
carlength           0
carwidth            0
carheight           0
curbweight          0
enginetype          0
cylindernumber      0
enginesize          0
fuelsystem          0
boreratio           0
stroke              0
compressionratio    0
horsepower          0
peakrpm             0
citympg             0
highwaympg          0
price               0
dtype: int64

2.1.3 Untersuchung von cylindernumber und doornumber: sind als Kategorie angegeben, in Zahl umwandeln¶

  1. Es ist anzunehmen, dass vier Tür mehr als zwei Tür kostet. ['two' 'four'] in Zahlen: 2, 4
  2. Es ist anzunehmen, dass der Preis mit der Anzahl der Zylinder steigt. ['four', 'six', 'five', 'three', 'twelve', 'two', 'eight']: z.B. 'two' in 2, 'twelve' in 12 umwandeln: 2,3,4,5,6,8,12
In [52]:
# zeige welche Anzahl von Tür ein Auto hat. z.B. 2,3,5 
# zeige auch die Anzahl von Autos  von 2 und 4 Türen
print(df.doornumber.unique())
print(df.groupby(['doornumber']).count())
df.groupby(['doornumber']).mean(numeric_only=True)
['two' 'four']
            car_ID  symboling  CarName  fueltype  aspiration  carbody  \
doornumber                                                              
four           115        115      115       115         115      115   
two             90         90       90        90          90       90   

            drivewheel  enginelocation  wheelbase  carlength  carwidth  \
doornumber                                                               
four               115             115        115        115       115   
two                 90              90         90         90        90   

            carheight  curbweight  enginetype  cylindernumber  enginesize  \
doornumber                                                                  
four              115         115         115             115         115   
two                90          90          90              90          90   

            fuelsystem  boreratio  stroke  compressionratio  horsepower  \
doornumber                                                                
four               115        115     115               115         115   
two                 90         90      90                90          90   

            peakrpm  citympg  highwaympg  price  
doornumber                                       
four            115      115         115    115  
two              90       90          90     90  
Out[52]:
car_ID symboling wheelbase carlength carwidth carheight curbweight enginesize boreratio stroke compressionratio horsepower peakrpm citympg highwaympg price
doornumber
four 112.965217 0.104348 101.133913 178.388696 66.300000 54.915652 2646.260870 127.669565 3.358261 3.252348 10.766087 99.686957 5020.869565 25.147826 30.530435 13501.152174
two 90.266667 1.766667 95.718889 168.504444 65.406667 52.203333 2439.677778 125.933333 3.293333 3.259333 9.345778 109.777778 5258.333333 25.311111 31.033333 12989.924078
In [53]:
# zeige welche Zylinder Kategorie es gibt
df.cylindernumber.unique()
Out[53]:
array(['four', 'six', 'five', 'three', 'twelve', 'two', 'eight'],
      dtype=object)
In [54]:
# wie schon erwähnt. es ist sinnvoll, Anzahl von Tür und Zylinder in Zahl umzuwandeln
map_list= {'two':2,'three':3,'four':4, 'five':5,'six':6,'eight':8,'twelve':12}
if( door_mapCt1==0): #verhindern nochmaliges Umwandeln
  df['cylindernumber'] = df['cylindernumber'].map(map_list).astype(int)
  df['doornumber'] = df['doornumber'].map(map_list).astype(int)

door_mapCt1=1
print(df.cylindernumber)
print(df.doornumber)
0      4
1      4
2      6
3      4
4      5
      ..
200    4
201    4
202    6
203    6
204    4
Name: cylindernumber, Length: 205, dtype: int64
0      2
1      2
2      2
3      4
4      4
      ..
200    4
201    4
202    4
203    4
204    4
Name: doornumber, Length: 205, dtype: int64

Anwendungvon df.describe()¶

Die Methode df.describe() in Pandas liefert eine statistische Zusammenfassung der numerischen Spalten eines DataFrames.
Standardmäßig zeigt sie folgende Kennzahlen:

  • count: Anzahl der nicht fehlenden Werte
  • mean: Mittelwert der Spalte
  • std: Standardabweichung
  • min: Minimalwert
  • 25%, 50%, 75%: Quartile (25%, Median, 75%)
  • max: Maximalwert

Warum ist df.describe() sinnvoll für Data Analysts?¶

1. Schneller Überblick über die Daten¶

Man sieht sofort Mittelwerte, Streuung, Minimal- und Maximalwerte.

2. Datenqualität prüfen¶

Wenn count für eine Spalte kleiner als die Anzahl der Zeilen ist, gibt es fehlende Werte (NaN oder None).

3. Vergleich zwischen Spalten¶

Beispiel: citympg und highwaympg

  • df.describe() ermöglicht es, schnell zu erkennen, welche Spalten stark variieren und welche konstant sind.
  • Man kann erste Korrelationen und Ähnlichkeiten zwischen Spalten identifizieren.
In [55]:
# zeige die statistische Zusammenfassung
df.describe()
Out[55]:
car_ID symboling doornumber wheelbase carlength carwidth carheight curbweight cylindernumber enginesize boreratio stroke compressionratio horsepower peakrpm citympg highwaympg price
count 205.000000 205.000000 205.000000 205.000000 205.000000 205.000000 205.000000 205.000000 205.000000 205.000000 205.000000 205.000000 205.000000 205.000000 205.000000 205.000000 205.000000 205.000000
mean 103.000000 0.834146 3.121951 98.756585 174.049268 65.907805 53.724878 2555.565854 4.380488 126.907317 3.329756 3.255415 10.142537 104.117073 5125.121951 25.219512 30.751220 13276.710571
std 59.322565 1.245307 0.994966 6.021776 12.337289 2.145204 2.443522 520.680204 1.080854 41.642693 0.270844 0.313597 3.972040 39.544167 476.985643 6.542142 6.886443 7988.852332
min 1.000000 -2.000000 2.000000 86.600000 141.100000 60.300000 47.800000 1488.000000 2.000000 61.000000 2.540000 2.070000 7.000000 48.000000 4150.000000 13.000000 16.000000 5118.000000
25% 52.000000 0.000000 2.000000 94.500000 166.300000 64.100000 52.000000 2145.000000 4.000000 97.000000 3.150000 3.110000 8.600000 70.000000 4800.000000 19.000000 25.000000 7788.000000
50% 103.000000 1.000000 4.000000 97.000000 173.200000 65.500000 54.100000 2414.000000 4.000000 120.000000 3.310000 3.290000 9.000000 95.000000 5200.000000 24.000000 30.000000 10295.000000
75% 154.000000 2.000000 4.000000 102.400000 183.100000 66.900000 55.500000 2935.000000 4.000000 141.000000 3.580000 3.410000 9.400000 116.000000 5500.000000 30.000000 34.000000 16503.000000
max 205.000000 3.000000 4.000000 120.900000 208.100000 72.300000 59.800000 4066.000000 12.000000 326.000000 3.940000 4.170000 23.000000 288.000000 6600.000000 49.000000 54.000000 45400.000000

Überblick über die Anzahl der eindeutigen Werte (unique values)¶

Die Methode df.nunique() in Pandas gibt die Anzahl der eindeutigen Werte jeder Spalte eines DataFrames zurück.


📌 Warum ist df.nunique() sinnvoll für die Datenanalyse?¶

  1. Identifikation von kategorialen Spalten

    • Spalten mit wenigen eindeutigen Werten sind oft kategorial (z. B. Geschlecht, Region).
    • Erkennt Variabilität in numerischen Spalten → wenn eine numerische Spalte nur wenige eindeutige Werte hat, könnte sie diskret oder ein Dummy-Feature sein.
  2. Datenbereinigung und Feature Engineering

    • Hilft, redundante Spalten oder Spalten mit konstanten Werten zu erkennen.
    • Unterstützt Entscheidungen für Encoding-Strategien, z. B. One-Hot-Encoding oder Ordinal-Encoding.
  3. Schneller Überblick über die Datenstruktur

    • Data Analysts bekommen sofort ein Gefühl dafür, wie divers die Werte in jeder Spalte sind,
      ohne alle Daten einzeln prüfen zu müssen.
In [11]:
# Überblick über die Anzahl der eindeutigen Werte
df.nunique()
Out[11]:
car_ID              205
symboling             6
CarName             147
fueltype              2
aspiration            2
doornumber            2
carbody               5
drivewheel            3
enginelocation        2
wheelbase            53
carlength            75
carwidth             44
carheight            49
curbweight          171
enginetype            7
cylindernumber        7
enginesize           44
fuelsystem            8
boreratio            38
stroke               37
compressionratio     32
horsepower           59
peakrpm              23
citympg              29
highwaympg           30
price               189
dtype: int64

2.1.4 Erstellung von Histogrammen für numerische Werte¶

Ein Histogramm stellt die Häufigkeitsverteilung der Werte einer numerischen Spalte grafisch dar.
Die Werte werden in sogenannte Bins (Intervalle) unterteilt, und für jedes Intervall wird angezeigt, wie viele Datenpunkte darin liegen.

Mit Histogrammen kann man auf einen Blick erkennen:

  • die Verteilung der Daten (gleichmäßig, schief, normalverteilt, etc.),
  • das Vorhandensein von Ausreißern oder ungewöhnlichen Werten (Grenze: -3std < x < +3std),
  • Symmetrie oder Asymmetrie der Datenverteilung.

Für Data Analysts ist dies sinnvoll, da Histogramme helfen, die Daten besser zu verstehen, die Wahl geeigneter Transformationen zu planen (z. B. log, sqrt) und Vorverarbeitungsmaßnahmen für Machine-Learning-Modelle abzuleiten.

2.1.4.1 Analyse der Histogramme der Fahrzeugmerkmale¶

Die bereitgestellten Histogramme geben einen Überblick über die Verteilung verschiedener numerischer Merkmale im Datensatz.


Aussage über die Verteilung:¶

  1. symboling
  • Verteilung: Ungleichmäßig, mehrere Peaks; häufigste Werte bei 0 und 1, signifikante Anteile bei 2 und 3, geringere Anteile bei -1, -2 und 4.
  • Interpretation: Könnte eine Risikobewertung oder Kategorisierung darstellen. Die Konzentration um 0 und 1 zeigt, dass die meisten Fahrzeuge in diesen Kategorien liegen, extreme Werte sind selten.
  1. doornumber
  • Verteilung: Bimodal mit Spitzen bei 2 und 4.
  • Interpretation: Die Mehrheit der Fahrzeuge hat entweder 2 oder 4 Türen; 4-türige Fahrzeuge sind etwas häufiger.
  1. cylindernumber
  • Verteilung: Stark rechtsschief, dominanter Peak bei 4 Zylindern; kleinere Anteile bei 5, 6, 8 und sehr wenige bei 3, 10, 12.
  • Interpretation: 4-Zylinder-Motoren sind am häufigsten, größere Motoren selten.
  1. wheelbase
  • Verteilung: Nahezu glockenförmig, leicht rechtsschief, Hauptteil zwischen 90-110, Peak um 95-100.
  • Interpretation: Die meisten Fahrzeuge haben einen mittleren Radstand.
  1. carlength
  • Verteilung: Glockenförmig, leicht rechtsschief, meist zwischen 160-180, Peak um 170.
  • Interpretation: Fahrzeuglängen konzentrieren sich auf mittlere Werte.
  1. carwidth
  • Verteilung: Glockenförmig, leicht rechtsschief, Peak um 65.
  • Interpretation: Fahrzeugbreiten liegen in einem engen Bereich.
  1. carheight
  • Verteilung: Relativ gleichmäßig, leicht multimodal, Bereich 50-60, mehrere Peaks.
  • Interpretation: Fahrzeughöhen variieren stärker als Länge oder Breite.
  1. curbweight
  • Verteilung: Leicht rechtsschief, Hauptteil zwischen 2000-3000, Peak um 2200-2400.
  • Interpretation: Die meisten Fahrzeuge sind im unteren bis mittleren Gewichtbereich.
  1. enginesize
  • Verteilung: Stark rechtsschief, dominanter Peak bei kleineren Motorgrößen (~100).
  • Interpretation: Kleine Motoren sind häufig, große Motoren selten.
  1. boreratio
  • Verteilung: Gleichmäßig, leicht multimodal, Bereich 3.0-3.5.
  • Interpretation: Zeigt verschiedene gängige Motorbohrungsverhältnisse.
  1. stroke
  • Verteilung: Glockenförmig, leicht linksschief, Peak um 3.3.
  • Interpretation: Hub konzentriert sich auf einen klaren Bereich.
  1. compressionratio
  • Verteilung: Stark rechtsschief, dominanter Peak 9-10, kleiner zweiter Peak 20-22.
  • Interpretation: Die meisten Autos haben typische Benzin-Kompressionsverhältnisse; höhere Werte deuten auf Diesel hin.
  1. horsepower
  • Verteilung: Stark rechtsschief, dominanter Peak bei 60-80 PS.
  • Interpretation: Die Mehrheit der Fahrzeuge hat geringere Leistung; sehr starke Fahrzeuge sind selten.
  1. peakrpm
  • Verteilung: Glockenförmig, leicht rechtsschief, Peak um 5000 U/min.
  • Interpretation: Maximaldrehzahl liegt bei den meisten Fahrzeugen um 5000 U/min.
  1. citympg
  • Verteilung: Stark rechtsschief, Peak bei 20-25 MPG.
  • Interpretation: Die meisten Fahrzeuge haben durchschnittlichen Stadtverbrauch, sparsame Autos sind seltener.
  1. highwaympg
  • Verteilung: Stark rechtsschief, Peak bei 25-30 MPG.
  • Interpretation: Autobahnverbrauch ist tendenziell besser als Stadtverbrauch; sehr sparsame Fahrzeuge sind selten.

2.1.4.2 Zusammenfassende Beobachtungen¶

  • Rechtsschiefe Verteilungen: Viele Merkmale (z.B. Zylinderzahl, Motorgröße, Leistung, Curbweight, City/Highway MPG) weisen rechtsschiefe Verteilungen auf. Hohe Werte sind selten.
  • Kategoriale/Diskrete Werte: "Symboling" und "doornumber" sind diskret oder kategorial.
  • Normalverteilungsähnlichkeit: Merkmale wie "wheelbase", "carlength", "carwidth", "peakrpm", "stroke" ähneln einer Normalverteilung.
  • Bimodale Verteilungen: "doornumber" und "compressionratio" zeigen zwei Gruppen (z.B. 2- vs. 4-türige Autos, Benzin- vs. Diesel-Motoren).

Fazit:
Die Histogramme liefern einen wertvollen Überblick über die Verteilung der Fahrzeugmerkmale, helfen, Ausreißer zu erkennen und identifizieren typische Wertebereiche sowie gemeinsame Muster im Datensatz.

In [56]:
# Feature engneering: 
# Histogram für numerische Werte erstellen und darstellen
numerical_columns = ['symboling','doornumber','cylindernumber','wheelbase', 'carlength', 'carwidth', 'carheight', 'curbweight',
                     'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower',
                     'peakrpm', 'citympg', 'highwaympg']

cols = 5
rows = int(np.ceil(len(numerical_columns) / cols))

fig = make_subplots(rows=rows, cols=cols, subplot_titles=numerical_columns)

for i, feature in enumerate(numerical_columns):
    row = i // cols + 1
    col = i % cols + 1

    fig.add_trace(
    go.Histogram(
        x=df[feature],
        nbinsx=25,
        name=feature,
        marker=dict(
            line=dict(
                color='black',  # Linienfarbe
                width=1         # Liniendicke
            )
        )
    ),
    row=row, col=col
    )


fig.update_layout(
    height=300 * rows,
    width=1400,
    title_text="Histogramme der numerischen Features",
    showlegend=False
)

fig.show()
No description has been provided for this image

2.1.5 Histogram von Autopreisen: hier von besonderem Interesse¶

Es geht um folgende Rahmenkennzahlen der Autopreise

Min.: 5118€¶

Max.: 45400€¶

mean: 13276€¶

midian: 10295€¶

Es ist zu erkennen, dass die meisten Autos im Preisbereich zwischen 6.000 und 20.000 $ liegen.
Das bedeutet, dass die Daten überwiegend Fahrzeuge aus dem niedrigen und mittleren Preissegment abbilden.
Daraus lässt sich schließen, dass die Modellierung und die Preisvorhersage in erster Linie auf diese Segmente ausgelegt ist.

Für das hochpreisige Segment sind dagegen deutlich weniger Datenpunkte vorhanden, was die Prognosegenauigkeit in diesem Bereich einschränken kann.

Auf Grundlage dieser Erkenntnisse muss entschieden werden, ob die Daten für das hochpreisige Segment überhaupt sinnvoll für die Modellierung und Preisvorhersage genutzt werden können.

Da in diesem Bereich nur sehr wenige Datenpunkte vorliegen, besteht die Gefahr, dass das Modell hier keine zuverlässigen Zusammenhänge lernen kann.
Dies kann die Qualität und Aussagekraft des Modells im oberen Preissegment deutlich verschlechtern und zu ungenauen oder verzerrten Vorhersagen führen.

Eine mögliche Konsequenz wäre, das Modell bewusst auf das niedrige und mittlere Preissegment zu beschränken oder für das hochpreisige Segment zusätzliche Datenquellen zu erschließen.

In [57]:
# Analyse von Preis: Histogram für Preise

print("-- Überblick über statistische Werte von Preis --")

print("min.:", df['price'].min())
print("max.:",df['price'].max())
print("mean:",df['price'].mean())
print("median:",df['price'].median())


fig = px.histogram(df, x='price', nbins=40, marginal="rug", title='Distribution of Price', opacity=0.8)

fig.update_layout(
    xaxis_title='Price',
    yaxis_title='Count',
    height=600  # Höhe des Plots
)

fig.update_xaxes(
    tickmode='linear',
    tick0=0,
    dtick=2000
)

fig.show()
-- Überblick über statistische Werte von Preis --
min.: 5118.0
max.: 45400.0
mean: 13276.710570731706
median: 10295.0
No description has been provided for this image

2.1.6 Bar-Darstellung der kategorischen Daten¶

In [58]:
print(df['fuelsystem'].unique())
print(df['enginetype'].unique())
print(df['carbody'].unique())
['mpfi' '2bbl' 'mfi' '1bbl' 'spfi' '4bbl' 'idi' 'spdi']
['dohc' 'ohcv' 'ohc' 'l' 'rotor' 'ohcf' 'dohcv']
['convertible' 'hatchback' 'sedan' 'wagon' 'hardtop']
In [59]:
import plotly.subplots as sp

categorical_columns = ['fueltype', 'aspiration', 'carbody', 'drivewheel',
                       'enginelocation', 'enginetype', 'fuelsystem']

df_cat = df.copy()
for c in categorical_columns:
    df_cat[c] = (df_cat[c].astype('string').str.strip().fillna('dl'))

fig = sp.make_subplots(rows=3, cols=3,
    subplot_titles=[col.capitalize() for col in categorical_columns])

row, col = 1, 1
for column in categorical_columns:
    vc = (df_cat[column]
          .value_counts(dropna=False)   # alle mitnehmen
          .sort_index())                # fixe Reihenfolge (oder sort_values())

    cats = list(map(str, vc.index))     # Kategorien als Strings

    fig.add_trace(
        go.Bar(x=cats, y=vc.values, name=column),
        row=row, col=col
    )

    fig.update_xaxes(
        type='category',
        title_text=column.capitalize(),
        categoryorder="array",
        categoryarray=cats,
        tickangle=45,
        row=row, col=col
    )
    fig.update_yaxes(title_text="Count", row=row, col=col)

    col += 1
    if col > 3:
        col = 1
        row += 1

fig.update_layout(height=900, width=1300, showlegend=False,
                  title_text="Categorical Features Count Plots",
                  bargap=0.15)
fig.show()
No description has been provided for this image

ich habe kategorischen Variablen zunächst exploriert (Häufigkeiten, dominante Kategorien, seltene Kategorien) und anschließend überlegt, ob und wie man diese in numerische Faktoren übersetzen könnte.

Die Logik:

Dominante Kategorie = Basisfaktor 1.0

Weniger dominante Kategorie = etwas >1

Seltene oder fachlich teurere Kategorie = größerer Faktor

Teilweise fachlich begründet (z. B. Diesel teurer als Gas).

Das ist ein Mix aus fachlichem Wissen und heuristischer Gewichtung – allerdings ist das nicht die Standardmethode im Machine Learning (da nimmt man z. B. One-Hot-Encoding, Ordinal-Encoding oder Target-Encoding). Trotzdem finde ich sinnvoll, diesen Ansatz als Überlegung sauber zu dokumentieren.

Kategorien und Überlegung zur Faktorisierung

Bei der Analyse der kategorialen Merkmale fällt auf, dass manche Kategorien sehr dominant sind (z. B. Gas bei Fueltype, OHC bei Engine Type, Front bei Engine Location), während andere nur selten vorkommen. Für Machine Learning ist es jedoch erforderlich, diese Kategorien in numerische Werte umzuwandeln.

Eine Überlegung ist, Faktoren zu vergeben:

Dominante Kategorie → Basiswert 1.0

Weniger dominante, aber relevante Kategorie → Wert leicht über 1.0

Sehr seltene Kategorie → noch etwas größerer Faktor

Zusätzlich: fachliche Begründung (z. B. Diesel i. d. R. teurer als Benzin).

Tabellarische Zusammenfassung:

Feature Kategorien (mit Häufigkeit) Faktor-Überlegung / Begründung Beispiel-Faktoren
Fueltype Gas (185), Diesel (20) Diesel oft teurer als Benzin Gas = 1.0, Diesel = 1.2
Aspiration std (168), turbo (37) Turbo = leistungssteigernd, höherer Preis std = 1.0, turbo = 1.3
Carbody convertible (6), hardtop (8), hatchback (70), sedan (96), wagon (25) Sedan als Basis, convertible/hardtop teurer, hatchback günstiger sedan = 1.0, hatchback = 1.1, wagon = 1.2, convertible = 2.0, hardtop = 2.3
Drivewheel fwd (120), rwd (76), 4wd (9) fwd = Standard, rwd = sportlicher/teurer, 4wd selten/luxus fwd = 1.0, rwd = 1.2, 4wd = 2.0
Enginelocation front (202), rear (3) Rear = selten, meist Sportwagen → teurer front = 1.0, rear = 1.2
Enginetype ohc (148), dohc (12), dohcv (1), l (12), ohcf (15), ohcv (13), rotor (4) ohc = Standard, andere Bauarten seltener/teurer ohc = 1.1, l = 1.0, dohc = 1.3, dohcv = 1.4, ohcf = 1.1, ohcv = 1.2, rotor = 1.5
Fuelsystem mpfi (94), 2bbl (66), idi (20), 1bbl (11), spdi (9), 4bbl (3), mfi (1), spfi (1) mpfi als Basis, seltenere Systeme teurer oder spezieller mpfi = 1.0, 2bbl = 1.1, idi = 1.3, 1bbl = 1.5, spdi = 1.7, 4bbl = 1.4, mfi = 1.6, spfi = 1.8

Bewertung des Ansatzes

Dieser Ansatz liefert eine erste Annäherung an eine numerische Abbildung kategorialer Variablen.

Allerdings ist die Faktorisierung subjektiv und basiert auf einer Mischung aus fachlichem Wissen und Häufigkeit der Kategorien.

In der Praxis würden Datenanalysten eher Verfahren wie One-Hot-Encoding oder Target-Encoding verwenden, da diese Methoden objektiver und reproduzierbarer sind.

Meine Überlegung stellt einen spezifischen Ansatz dar und dient in erster Linie dazu, ein datenbasiertes, besseres Verständnis der Merkmale zu gewinnen. Es handelt sich dabei nicht um ein endgültiges Vorgehen für die Modellierung, sondern um eine gedankliche Stütze, wie man als Datenanalyst die Daten interpretieren und deren Einfluss einschätzen kann.

Für die eigentliche Modellierung werde ich weiterhin die etablierten Standardverfahren einsetzen.

2.1.7 Überblick über die Autohersteller:¶

Hier werden die Top-20 Autohersteller in der Daten dargestellt. z.B. Das Modell Peugeot 504 kommt 6-mal im Datensatz vor.

In [60]:
n = 20  # Number of top car models to plot
top_car_models = df['CarName'].value_counts().head(n)

plt.figure(figsize=(10, 6))
sns.barplot(x=top_car_models.values, y=top_car_models.index)
plt.title(f'Top {n} Car Models by Frequency')
plt.xlabel('Frequency')
plt.ylabel('Car Model')
plt.tight_layout()
plt.show()
No description has been provided for this image

2.1.8 Berechnung von Durchschnittpreis für Top-20 Autohersteller¶

Hier werden die teuersten Top-20 Autohersteller in der Daten dargestellt.

In [61]:
# Calculate average price for each car model
avg_prices_by_car = df.groupby('CarName')['price'].mean().sort_values(ascending=False)

# Plot top N car models by average price
n = 20  # Number of top car models to plot
top_car_models = avg_prices_by_car.head(n)

plt.figure(figsize=(10, 6))
sns.barplot(x=top_car_models.values, y=top_car_models.index)
plt.title(f'Top {n} Car Models by Average Price')
plt.xlabel('Average Price')
plt.ylabel('Car Model')
plt.tight_layout()
plt.show()
No description has been provided for this image

2.1.9 Durchschnittspreise der Hersteller im unteren Preissegment (günstigste 20 Autohersteller)¶

Hier werden die günstisten Top-20 Autohersteller in der Daten dargestellt.

In [62]:
# Calculate average price for each car model
avg_prices_by_car = df.groupby('CarName')['price'].mean().sort_values(ascending=True)

# Plot top N car models by average price
n = 20 
top_car_models = avg_prices_by_car.head(n)

plt.figure(figsize=(10, 6))
sns.barplot(x=top_car_models.values, y=top_car_models.index)
plt.title(f' Cheapest {n} Car Models by Average Price')
plt.xlabel('Average Price')
plt.ylabel('Car Model')
plt.tight_layout()
plt.show()
No description has been provided for this image

2.1.10 Kategorische Merkmale vs. Preis: Boxplot¶

Der Boxplot ist ein zentrales Werkzeug in der Datenanalyse. Er visualisiert die Verteilung von Daten und macht wichtige Eigenschaften wie Lage, Streuung und mögliche Ausreißer auf einen Blick erkennbar.¶

1. Interquartile Range (IQR)

Der Interquartilbereich (IQR) beschreibt die Spannweite der mittleren 50 % der Daten.

Q1 (25. Perzentil): Wert, unter dem 25 % der Daten liegen.

Q2 (Median, 50. Perzentil): Wert, unter dem 50 % der Daten liegen.

Q3 (75. Perzentil): Wert, unter dem 75 % der Daten liegen.

Formel für IQR:

IQR=Q3−Q1

Der IQR gibt also die Spannweite des „mittleren Blocks“ der Daten an – er ignoriert die extremen Ausreißer.

Beispiel: Wenn Q1 = 10 und Q3 = 20 → IQR = 20 − 10 = 10.

2. Whiskers (typisch 1.5×IQR)

Die Whiskers in einem Boxplot zeigen die Daten „außerhalb“ des mittleren Bereichs, aber ohne Ausreißer.

Standardregel:

Unteres Whisker-Limit: Q1−1.5×IQR

Oberes Whisker-Limit: Q3+1.5×IQR

Alles unterhalb von Q1 − 1.5×IQR oder oberhalb von Q3 + 1.5×IQR wird als Ausreißer markiert (oft Punkte außerhalb des Whiskers).

Beispiel:

Q1 = 10, Q3 = 20 → IQR = 10

Unteres Limit: 10 − 1.5×10 = −5

Oberes Limit: 20 + 1.5×10 = 35

Wenn deine Daten von 0 bis 40 reichen:

Whisker zeigen nur 0 bis 35

Werte >35 (z. B. 37, 40) sind Ausreißer.

3. Visualisierung im Boxplot

      ┌─────────────┐  ← Q3 (75%)
      │             │
      │    Box      │  ← mittlere 50% (Q1–Q3)
      │             │
      └─────────────┘  ← Q1 (25%)
    Whisker:   │
               │
               └── Punkte = Ausreißer (>1.5×IQR)

image.png

Zusammenhang zwischen Whisker und 3-facher Standardabweichung¶

Da das Thema Ausreißererkennung hier behandelt wird, ist es sinnvoll einen Vergeleich der zwei Methoden zur Ausreißererkennung zu machen


1️⃣ Whisker (Boxplot) – IQR-Methode¶

  • IQR = Interquartilsabstand = Q3 − Q1
  • Unteres Whisker-Limit: Q1 - 1.5 × IQR
  • Oberes Whisker-Limit: Q3 + 1.5 × IQR

Eigenschaften:

  • Robust gegenüber Ausreißern, da nur die mittleren 50 % der Daten betrachtet werden.
  • Funktioniert auch bei schiefen oder nicht-normalverteilten Daten gut.

2️⃣ 3-fache Standardabweichung – Z-Score-Methode¶

  • Mittelwert: μ
  • Standardabweichung: σ
  • Unteres Limit: μ - 3σ
  • Oberes Limit: μ + 3σ

Eigenschaften:

  • Annahme: Daten sind normalverteilt.
  • Laut 68–95–99.7-Regel liegen ~99.7 % der Werte innerhalb von ±3σ.
  • Ausreißer sind Werte >3σ vom Mittelwert entfernt.

3️⃣ Vergleich / Zusammenhang¶

Aspekt IQR (Whisker) 3σ-Methode
Basis Quartile (Q1, Q3) Mittelwert & Standardabweichung
Robustheit Sehr robust gegenüber Ausreißern Weniger robust, Ausreißer beeinflussen σ
Verteilung Keine Annahmen Annahme: Normalverteilung
Typische Erkennung Werte >1.5×IQR vom Quartil entfernt Werte >3σ vom Mittelwert entfernt
Sensitivität Moderate Ausreißer Extreme Ausreißer bei Normalverteilung

Fazit:

  • Beide Methoden erkennen Ausreißer, aber Whisker basiert auf der mittleren Hälfte der Daten und ist robuster bei Schiefe oder Extremwerten.
  • Die 3σ-Regel eignet sich besonders für normalverteilte Daten.
  • Bei symmetrischer, ungefähr normalverteilter Datenmenge sind die Grenzen oft ähnlich, aber nicht identisch.

4. Intepretation der Ergebnisse vom Boxplot:

4.1 fueltype vs. Price

Gas ist der häufigste Treibstofftyp. Die Preise liegen meist zwischen 6.000 – 20.000 $,

mit Ausreißern bis über 30.000 $.

Diesel-Autos scheinen im Median etwas teurer als Gas-Autos, aber die Stichprobe ist kleiner.

👉 Interpretation: Dieselkraftstoff korreliert mit tendenziell höheren Preisen.

4.2 aspiration vs. Price

Standard-Aspiration (std: Luftzufuhr ohne Turbo oder Kompressor) hat eine breite Preisverteilung, Median im mittleren Bereich.

Turbo-Autos sind im Median teurer, aber auch mit hoher Varianz.

👉 Interpretation: Turbo-Autos könnten ein Premium-Segment darstellen.

4.3 carbody vs. Price

Sedan und Hatchback dominieren und liegen im mittleren bis unteren Preisbereich.

Convertible (Cabrio) und Hardtop zeigen höhere Medianpreise und größere Varianz.

👉 Interpretation: Die Karosserieform hat deutlichen Einfluss auf den Preis, Premium-Karosserien (Cabrio, Hardtop) → teurer.

4.4. drivewheel vs. Price

RWD (Heckantrieb) → tendenziell teurere Autos, größerer Median.

FWD (Frontantrieb) → günstigere Autos, am häufigsten vertreten.

4WD (Allrad) → wenige Datenpunkte, Median im mittleren Bereich.

👉 Interpretation: Heckantrieb wird eher bei teuren Autos verwendet (Sportwagen/Luxusautos).

4.5 enginelocation vs. Price

Front → fast alle Autos, Preise meist unter 20.000 $.

Rear (Heckmotor) → sehr wenige Autos, aber deutlich höherer Preisbereich.

👉 Interpretation: Heckmotor ist selten, typisch für Sportwagen, deutlich teurer.

4.6 enginetype vs. Price

ohc (Overhead Camshaft) → am häufigsten, Preise im unteren bis mittleren Bereich.

dohcv und rotor → selten, aber sehr hochpreisig.

ohcf und ohcv → mittleres Segment.

👉 Interpretation: Bestimmte Motortypen (z. B. dohc, rotor) sind mit Premiumautos assoziiert.

4.7 fuelsystem vs. Price

Sehr viele Systeme im Vergleich, z. B. mpfi (häufig) → mittlerer Preisbereich.

idi und spdi → sehr kleine Gruppen, mit hohen Ausreißern.

👉 Interpretation: Das Kraftstoffsystem spielt eine Rolle, aber die meisten Systeme liegen im günstigen Bereich; nur wenige Systeme (idi, spdi) korrelieren mit hohen Preisen.

In [63]:
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from math import ceil

# Categorical Feature vs. Price
n_cols = 3
n_rows = ceil(len(categorical_columns) / n_cols)

fig = make_subplots(rows=n_rows, cols=n_cols, subplot_titles=categorical_columns)

for idx, feature in enumerate(categorical_columns):
    row = idx // n_cols + 1
    col = idx % n_cols + 1
    
    # Einzelner Boxplot mit plotly.express erzeugen
    box_fig = px.box(df, x=feature, y='price', points='outliers', color_discrete_sequence=["blue"])
    
    # Traces in die Subplots einfügen
    for trace in box_fig.data:
        fig.add_trace(trace, row=row, col=col)

fig.update_layout(height=450 * n_rows, width=500 * n_cols, showlegend=False)
fig.show()
No description has been provided for this image

2.1.11 Numerische Werte mit Bar-Darstellung¶

Normalerweise werden numerische Werte mit einem Histogramm dargestellt, da dies die Verteilung der Werte zeigt. In diesem Fall werden sie jedoch mit einem Balkendiagramm (Barplot) abgebildet, um gezielt die Anzahl der Messwerte in bestimmten Kategorien oder Bereichen hervorzuheben und mehr Details sichtbar zu machen.

In [65]:
# jeder Werte zeigen, ähnlich wie histogramm (Werte aber zusammenfasst)

numerical_columns = ['symboling','doornumber','cylindernumber','wheelbase', 'carlength', 'carwidth', 'carheight', 'curbweight',
                     'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower',
                     'peakrpm', 'citympg', 'highwaympg']

single_color = 'blue'  # eine Farbe für alle Plots

fig = make_subplots(
    rows=8, cols=2,
    subplot_titles=[col.capitalize() for col in numerical_columns],
    horizontal_spacing=0.05,  # engerer horizontaler Abstand
    vertical_spacing=0.08
)

row, col = 1, 1
for col_name in numerical_columns:
    counts = df[col_name].value_counts().reset_index()
    counts.columns = [col_name, 'Count']

    fig.add_trace(
        go.Bar(
            x=counts[col_name],
            y=counts['Count'],
            text=counts['Count'],
            textposition='outside',
            marker_color=single_color
         
        ),
        row=row, col=col
    )

    col += 1
    if col > 2:  # nur 2 Spalten
        col = 1
        row += 1

fig.update_layout(
    height=2200, width=1600,  # Höhe und Breite anpassen
    title_text="Count Plots of Numerical Columns",
    showlegend=False
)

fig.show()
No description has been provided for this image

2.2 Explorative Datenanalyse (EDA)¶

Explorative Datenanalyse (EDA) ist ein methodischer Ansatz zur ersten Untersuchung von Datensätzen ohne vorgegebene Hypothesen. Ziel ist es, durch statistische und visuelle Methoden Muster, Zusammenhänge, Ausreißer und Datenstrukturen zu erkennen. Dabei hilft EDA, ein besseres Verständnis der Daten zu erlangen, Hypothesen zu generieren und die Datenqualität zu überprüfen.

EDA verwendet häufig Streudiagramme und Korrelationsanalysen, um Zusammenhänge zwischen Variablen zu untersuchen, während die deskriptive Statistik eher einfache Zusammenfassungen und Standardvisualisierungen liefert.

2.2.1 Streudiagramm: Preis (y-Achse) vs. numerische Spalte (x-Achse)¶

Ein Streudiagramm ist für die explorative Datenanalyse (EDA) sehr nützlich, weil es:

  • Zusammenhänge zwischen zwei numerischen Variablen sichtbar macht
  • Zeigt, ob eine positive, negative oder keine Korrelation vorliegt
  • Hilfreich ist, um Muster, Trends oder Ausreißer zu erkennen
  • Visuell sofort Einblicke in die Datenstruktur und mögliche Abhängigkeiten gibt

Kurz:
Ein Streudiagramm unterstützt in der EDA dabei, Beziehungen in den Daten zu verstehen und auf Basis dieser Erkenntnisse weitere Analyseschritte zu planen.

Interpretation der Scatterplots (Features vs. Preis)¶

1. Abmessungen¶

  • carlength vs price:
    Längere Autos → tendenziell teurer. Positiver Zusammenhang, aber mit Streuung.
  • carwidth vs price:
    Breitere Autos → deutlich teurer. Klarer linearer Trend.
  • carheight vs price:
    Kein klarer Trend. Manche hohe Autos sind teuer, andere nicht → schwacher Zusammenhang.
  • wheelbase vs price:
    Längere Radstände → etwas teurer, aber viele Ausreißer. Mittlerer Zusammenhang.

2. Gewicht & Motorgröße¶

  • curbweight vs price:
    Sehr starker positiver Zusammenhang → schwerere Autos sind teurer.
  • enginesize vs price:
    Sehr klarer Trend → größere Motoren bedeuten höhere Preise. Einer der besten Prädiktoren.
  • cylindernumber vs price:
    Mehr Zylinder → höhere Preise. Besonders von 3–4 (günstig) zu 6–8 (teuer). Wenige Autos mit 10–12 Zylindern, aber diese sind extrem teuer. Sehr guter Indikator.
  • boreratio vs price:
    Leichter positiver Trend. Große Streuung, daher nur mäßig stark.

3. Motorcharakteristik¶

  • stroke vs price:
    Kaum Zusammenhang, stark verstreute Werte.
  • compressionratio vs price:
    Kein klarer linearer Trend. Einzelne Autos mit hoher Kompression → teurer, aber unregelmäßig.
  • horsepower vs price:
    Sehr deutlicher positiver Zusammenhang. Leistungsstärkere Autos kosten klar mehr. Einer der stärksten Prädiktoren.
  • peakrpm vs price:
    Kein klarer Trend, Daten stark verstreut. Schwacher Prädiktor.

4. Verbrauchswerte¶

  • citympg vs price:
    Negativer Zusammenhang: sparsame Autos (hoher mpg) sind günstiger.
  • highwaympg vs price:
    Ähnlich wie citympg: höhere Effizienz → niedrigere Preise. Moderater negativer Zusammenhang.

5. Karosserie & Türen¶

  • doornumber vs price:
    Kein klarer Unterschied zwischen 2- und 4-Türern. Schwacher Einfluss auf den Preis.

✅ Gesamtbewertung¶

  • Sehr starke Prädiktoren (hohe Aussagekraft):
    enginesize, curbweight, horsepower, cylindernumber, carwidth, carlength.

  • Mittlere Prädiktoren:
    wheelbase, boreratio, citympg, highwaympg.

  • Schwache/kaum relevante Prädiktoren:
    carheight, stroke, compressionratio, peakrpm, doornumber.

In [66]:
# Feuture engineering for numerical_columns: Scatter plot

numerical_columns = ['doornumber','cylindernumber','wheelbase', 'carlength', 'carwidth', 'carheight', 'curbweight',
                     'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower',
                     'peakrpm', 'citympg', 'highwaympg']

target = 'price'
# Annahme: df ist dein DataFrame und features_for_scatter deine Liste der Features
# Beispielhafte Berechnung der Achsenbereiche, wie zuvor bestimmt (hier musst du ggf. deine realen Daten nutzen)
features_for_scatter = [f for f in numerical_columns if f != target]
print(features_for_scatter)
feature_ranges = {
    feature: (df[feature].min(), df[feature].max()) for feature in features_for_scatter
}

# Subplot-Layout mit 3 Spalten und entsprechender Zeilenanzahl
num_features = len(features_for_scatter)
num_cols = 3
num_rows = (num_features + num_cols - 1) // num_cols

fig = make_subplots(
    rows=num_rows, cols=num_cols,
    subplot_titles=[f"{feat} vs {target}" for feat in features_for_scatter],
    vertical_spacing=0.08  # Weniger Abstand zwischen den Reihen
)

#fig = make_subplots(rows=num_rows, cols=num_cols,subplot_titles=[f"{feat} vs {target}" for feat in features_for_scatter])

for i, feature in enumerate(features_for_scatter):
    row = i // num_cols + 1
    col = i % num_cols + 1
    # 5% Padding links und rechts für x-Achse
    x_min, x_max = feature_ranges[feature]
    x_range_pad = (x_max - x_min) * 0.05
    x_axis_range = [x_min - x_range_pad, x_max + x_range_pad]

    scatter = go.Scatter(x=df[feature], y=df[target], mode='markers', name=feature)
    fig.add_trace(scatter, row=row, col=col)

    # Setze die x-Achse des Subplots mit Range und angemessener Tick-Anzahl
    fig.update_xaxes(range=x_axis_range, row=row, col=col, nticks=5)

    # Label für jede Achse hinzufügen (optional)
    fig.update_xaxes(title_text=feature, row=row, col=col)
    fig.update_yaxes(title_text=target, row=row, col=col)

# Layout anpassen: Grösse, Abstand, Titel
fig.update_layout(height=300*num_rows, width=1300,  # statt 1000 jetzt 1200 für +20%
                  title_text="Scatterplots Features vs. price (3 Spalten Layout)",
                  showlegend=False,
                  margin=dict(l=40, r=40, t=80, b=40))


fig.show()
['doornumber', 'cylindernumber', 'wheelbase', 'carlength', 'carwidth', 'carheight', 'curbweight', 'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower', 'peakrpm', 'citympg', 'highwaympg']
No description has been provided for this image

2.2.2 Korrelationskarte (Heatmap)¶

1. Correlation Map of numerical_columns¶

Eine Korrelationskarte (Heatmap) zeigt die Zusammenhänge zwischen allen numerischen Spalten in einer Matrixform.

  • Jede Zelle enthält den Korrelationskoeffizienten (z. B. Pearson), der angibt, wie stark zwei Variablen linear miteinander zusammenhängen (Werte zwischen -1 und +1).
  • Positive Werte → beide Variablen steigen oder fallen gemeinsam.
  • Negative Werte → wenn eine steigt, sinkt die andere.
  • Werte nahe 0 → kein linearer Zusammenhang.

2. Warum ist das gut?¶

  • Erkennt starke Prädiktoren für die Zielvariable (z. B. price).
  • Hilft, Multikollinearität zu identifizieren (z. B. curbweight und enginesize könnten stark korrelieren).
  • Unterstützt bei der Feature Selection, um redundante Variablen zu vermeiden.
  • Liefert eine schnelle visuelle Übersicht, welche numerischen Features wichtig sein könnten.
In [67]:
# correlation map for numerical_columns:
numerical_columns1 = ['doornumber','cylindernumber','wheelbase', 'carlength', 'carwidth', 'carheight', 'curbweight',
                     'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower',
                     'peakrpm', 'citympg', 'highwaympg','price']

correlation_matrix = df[numerical_columns1].corr().round(2)
# Heatmap mit Plotly Express
fig = px.imshow(
    correlation_matrix,
    text_auto=True,         # Werte direkt in die Zellen schreiben
    color_continuous_scale='RdBu_r',  # entspricht 'coolwarm'
    title="Correlation Heatmap"
)
fig.update_layout(width=1200, height=1200)
fig.show()
No description has been provided for this image

3. Korrelation der Features mit dem Fahrzeugpreis¶

Feature (Merkmal) Korrelation Richtung
enginesize (Motorgröße) 0.87 positiv
curbweight (Leergewicht) 0.84 positiv
horsepower (PS) 0.81 positiv
carwidth (Fahrzeugbreite) 0.76 positiv
cylindernumber (Zylinderanzahl) 0.72 positiv
highwaympg (Verbrauch Autobahn) -0.71 negativ
citympg (Verbrauch Stadt) -0.70 negativ
carlength (Fahrzeuglänge) 0.68 positiv
wheelbase (Radstand) 0.57 positiv
boreratio (Bohrungsverhältnis) 0.55 positiv
carheight (Fahrzeughöhe) 0.11 positiv
peakrpm (max. Drehzahl) -0.09 negativ
stroke (Hub) 0.08 positiv
compressionratio (Verdichtungsverhältnis) 0.07 positiv
doornumber (Anzahl der Türen) 0.02 positiv

4. Analyse der Korrelationen mit dem Fahrzeugpreis¶

Die Analyse der Merkmale zeigt, dass bestimmte Fahrzeugparameter stark mit dem Fahrzeugpreis korrelieren:

  • Starke positive Korrelation:

    • Motorgröße (enginesize)
    • Leergewicht (curbweight)
    • PS (horsepower)
      Fahrzeuge mit größeren Motoren, höherem Gewicht und mehr Leistung tendieren dazu, teurer zu sein.
  • Weitere positive Einflüsse:

    • Fahrzeugbreite (carwidth)
    • Zylinderanzahl (cylindernumber)
    • Fahrzeuglänge (carlength)
    • Radstand (wheelbase)
    • Bohrungsverhältnis (boreratio)
      Diese Merkmale tragen ebenfalls positiv zum Preis bei, wenn auch moderater.
  • Negative Korrelation:

    • Verbrauch auf der Autobahn (highwaympg)
    • Verbrauch in der Stadt (citympg)
      Autos mit besserer Kraftstoffeffizienz sind tendenziell günstiger.
  • Geringe oder kaum relevante Korrelation:

    • Fahrzeughöhe (carheight)
    • Maximale Drehzahl (peakrpm)
    • Hub (stroke)
    • Verdichtungsverhältnis (compressionratio)
    • Anzahl der Türen (doornumber)
      Diese Merkmale haben nur einen schwachen Einfluss auf den Preis.

Fazit:
Motorgröße, Leergewicht und PS sind die stärksten positiven Indikatoren für den Fahrzeugpreis. Fahrzeugbreite und Zylinderanzahl sind ebenfalls wichtig. Die Kraftstoffeffizienz (highwaympg, citympg) wirkt stark negativ auf den Preis. Ähnlich wie im Streudiagramm spiegeln die Korrelationswerte die gleichen Zusammenhänge wider.

Mit dieser Erkenntnis lässt sich folgende Frage des Unternehmens gut beantworten:

Das Unternehmen möchte wissen:

  1. Welche Variablen sind signifikant für die Vorhersage des Autopreises?
  2. Wie gut erklären diese Variablen den Preis eines Autos?

3. Data Preparation (Datenaufbereitung)¶

Für Machine-Learning ist sehr wichtig, eine gute aufbereitete Daten zu haben, um ein allgemein aussagkräftiges Modell zu tranineren.

3.1. Datenbereinigung¶

  • car_ID: eine laufende Nummer, keine Bedeutung für Datenanalyse und maschinelles Lernen. Löschen
  • citympg und hightwaympg: die Verteilung wird noch mal genau angeschaut. Die Aussereisser werden bereinigt. siehe Punkt 3.5

3.2. Umwandlung von kategorischen Daten in numerischen Daten¶

Anzahl von Türen und Zylindern sind als kategorische Daten dargestellt. sie werden in Zahlen umgewandelt(schon erledigt in der Deskriptiven Analyse)

3.3. Zerlegung von kategorischen Daten in zwei Spalten: brand und model¶

In der Spalte 'CarName' sind Information über Marke und Modelle zusammengefasst. Sie sollen in zwei Spalten dargestellt werden

3.4. Anwendung von log(y),sqrt(y), x^2 usw. auf bestimmten Spalten: x: Preis, y: Werte in der Spalten¶

log_trans.png

  1. was bedeutet das z.B. log() Transformation: wenn y Werte (Enginesize) eine ideale Funktion von y= e^x, x(Preis), dannn log(y) wird dann linear zu x(Preis)

  2. Logarithmen helfen, schiefe (skewed) Verteilungen oder extrem große Wertebereiche, besser für Modelle zu optimieren.

  3. Das geiche gilt für andere funktionale Beziehung wie sqrt() usw.

    die zwei Spalten 'enginesize' und 'compressionratio' werden nach der Datenanalyse auf log() angewendet.

3.5. Analyse von Ausreißern in citympg und highwaympg¶

3.5.1 Statistische Betrachtung:¶

  1. Was ist die Standardabweichung (σ oder std)?
  • Die Standardabweichung misst, wie stark die einzelnen Werte einer Datenmenge im Durchschnitt von deren Mittelwert (Durchschnitt) abweichen.

  • Kleine Standardabweichung → die Werte liegen eng um den Mittelwert herum (geringe Streuung).

  • Große Standardabweichung → die Werte sind weit vom Mittelwert verteilt (hohe Streuung).

Formel (für eine Stichprobe):

Formel_std.png

wobei xi = einzelner Wert, xˉ = Mittelwert, n = Anzahl der Werte.

  1. Was bedeutet „3-fache Standardabweichung“? Wenn die Daten annähernd normalverteilt (Glockenkurve) sind, gilt die sogenannte 68–95–99,7-Regel:
  • ±1 σ → ca. 68% aller Werte liegen innerhalb einer Standardabweichung vom Mittelwert.

  • ±2 σ → ca. 95% aller Werte liegen innerhalb von zwei Standardabweichungen.

  • ±3 σ → ca. 99,7% aller Werte liegen innerhalb von drei Standardabweichungen.

  1. Das heißt: die Werte außer ±3σ werden beseitigt. Werte, die mehr als 3 Standardabweichungen vom Mittelwert entfernt sind, treten sehr selten auf (~0,3% Wahrscheinlichkeit). In der Statistik werden diese oft als Ausreißer betrachtet.

WP_Normalverteilung.jpg

In [23]:
#sns.pairplot(df)

3.5.2 Analyse von citympg und highwaympg:¶

In [68]:
# # Analyse von citympg und highwaympg:

print("-- statistische Werte von highwaympg --")

print("min.:", df['highwaympg'].min())
print("max.:",df['highwaympg'].max())
print("mean:",df['highwaympg'].mean())
print("median:",df['highwaympg'].median())
print("std:",df['highwaympg'].std())
print("6*std:",3*df['highwaympg'].std())

fig = px.histogram(df, x='highwaympg', nbins=30, marginal="rug", title='Distribution of highwaympg', opacity=0.8)

fig.update_layout(
    xaxis_title='highwaympg',
    yaxis_title='Count',    
    height=600  # Höhe des Plots
)

fig.update_xaxes(
    tickmode='linear',
    tick0=0,
    dtick=1  # Tick-Beschriftungen alle 2 Einheiten
)
fig.show()

print("-- statistische Werte von citympg --")

print("min.:", df['citympg'].min())
print("max.:",df['citympg'].max())
print("mean:",df['citympg'].mean())
print("median:",df['citympg'].median())
print("std:",df['citympg'].std())
print("6*std:",3*df['citympg'].std())

fig = px.histogram(df, x='citympg', nbins=30, marginal="rug", title='Distribution of citympg', opacity=0.8)

fig.update_layout(
    xaxis_title='citympg',
    yaxis_title='Count',    
    height=600  # Höhe des Plots
)

fig.update_xaxes(
    tickmode='linear',
    tick0=0,
    dtick=1  # Tick-Beschriftungen alle 2 Einheiten
)
fig.show()
-- statistische Werte von highwaympg --
min.: 16
max.: 54
mean: 30.75121951219512
median: 30.0
std: 6.886443130941824
6*std: 20.659329392825473
No description has been provided for this image
-- statistische Werte von citympg --
min.: 13
max.: 49
mean: 25.21951219512195
median: 24.0
std: 6.542141653001622
6*std: 19.626424959004865
No description has been provided for this image

3.5.3 Löschbedingung von Ausreißern: Spalte:'highwaympg'¶

mean: 30.75121951219512¶

std: 6.886443130941824¶

limit_right: mean+3*std=30.75+3 x 6.88(20.64)= ca. 52; Werte > 52 werden gelöscht: nur zwei Werte 53, 54 betroffen¶

limit_left: mean-+3*std= 30.75-3 x 6.88(20.64)= ca. 10; Werte < 10 werden gelöscht: keine Werte betroffen¶

In [69]:
# werden später beim Modelltraining benutzt
limit_highwaympg_r=52
limit_highwaympg_l=10
In [70]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 205 entries, 0 to 204
Data columns (total 26 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   car_ID            205 non-null    int64  
 1   symboling         205 non-null    int64  
 2   CarName           205 non-null    object 
 3   fueltype          205 non-null    object 
 4   aspiration        205 non-null    object 
 5   doornumber        205 non-null    int64  
 6   carbody           205 non-null    object 
 7   drivewheel        205 non-null    object 
 8   enginelocation    205 non-null    object 
 9   wheelbase         205 non-null    float64
 10  carlength         205 non-null    float64
 11  carwidth          205 non-null    float64
 12  carheight         205 non-null    float64
 13  curbweight        205 non-null    int64  
 14  enginetype        205 non-null    object 
 15  cylindernumber    205 non-null    int64  
 16  enginesize        205 non-null    int64  
 17  fuelsystem        205 non-null    object 
 18  boreratio         205 non-null    float64
 19  stroke            205 non-null    float64
 20  compressionratio  205 non-null    float64
 21  horsepower        205 non-null    int64  
 22  peakrpm           205 non-null    int64  
 23  citympg           205 non-null    int64  
 24  highwaympg        205 non-null    int64  
 25  price             205 non-null    float64
dtypes: float64(8), int64(10), object(8)
memory usage: 41.8+ KB

3.5.4 Zerlegung von CarName: brand and model¶

In [71]:
# separate brand and model 
split_cols = df['CarName'].str.split(' ', n=1, expand=True)
# Spalten zuordnen
df['brand'] = split_cols[0]        # erstes Wort
df['model'] = split_cols[1]  
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 205 entries, 0 to 204
Data columns (total 28 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   car_ID            205 non-null    int64  
 1   symboling         205 non-null    int64  
 2   CarName           205 non-null    object 
 3   fueltype          205 non-null    object 
 4   aspiration        205 non-null    object 
 5   doornumber        205 non-null    int64  
 6   carbody           205 non-null    object 
 7   drivewheel        205 non-null    object 
 8   enginelocation    205 non-null    object 
 9   wheelbase         205 non-null    float64
 10  carlength         205 non-null    float64
 11  carwidth          205 non-null    float64
 12  carheight         205 non-null    float64
 13  curbweight        205 non-null    int64  
 14  enginetype        205 non-null    object 
 15  cylindernumber    205 non-null    int64  
 16  enginesize        205 non-null    int64  
 17  fuelsystem        205 non-null    object 
 18  boreratio         205 non-null    float64
 19  stroke            205 non-null    float64
 20  compressionratio  205 non-null    float64
 21  horsepower        205 non-null    int64  
 22  peakrpm           205 non-null    int64  
 23  citympg           205 non-null    int64  
 24  highwaympg        205 non-null    int64  
 25  price             205 non-null    float64
 26  brand             205 non-null    object 
 27  model             203 non-null    object 
dtypes: float64(8), int64(10), object(10)
memory usage: 45.0+ KB

3.5.5 Datenbereinigung: zwei None festgestellt, NONE durch 'dl' (Default Label) ersetzen¶

In [28]:
nan_rows = df[df.isna().any(axis=1)].index
print(nan_rows)
Index([138, 141], dtype='int64')
In [29]:
df = df.fillna("dl")
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 205 entries, 0 to 204
Data columns (total 28 columns):
 #   Column            Non-Null Count  Dtype  
---  ------            --------------  -----  
 0   car_ID            205 non-null    int64  
 1   symboling         205 non-null    int64  
 2   CarName           205 non-null    object 
 3   fueltype          205 non-null    object 
 4   aspiration        205 non-null    object 
 5   doornumber        205 non-null    int64  
 6   carbody           205 non-null    object 
 7   drivewheel        205 non-null    object 
 8   enginelocation    205 non-null    object 
 9   wheelbase         205 non-null    float64
 10  carlength         205 non-null    float64
 11  carwidth          205 non-null    float64
 12  carheight         205 non-null    float64
 13  curbweight        205 non-null    int64  
 14  enginetype        205 non-null    object 
 15  cylindernumber    205 non-null    int64  
 16  enginesize        205 non-null    int64  
 17  fuelsystem        205 non-null    object 
 18  boreratio         205 non-null    float64
 19  stroke            205 non-null    float64
 20  compressionratio  205 non-null    float64
 21  horsepower        205 non-null    int64  
 22  peakrpm           205 non-null    int64  
 23  citympg           205 non-null    int64  
 24  highwaympg        205 non-null    int64  
 25  price             205 non-null    float64
 26  brand             205 non-null    object 
 27  model             205 non-null    object 
dtypes: float64(8), int64(10), object(10)
memory usage: 45.0+ KB

Originale Daten sichern, Originale Daten werden für machinelles Lernen wieder gebraucht¶

In [30]:
# originale Daten sichern, für weitere Modellevariante
df_cp= df.copy(deep= True)
df
Out[30]:
car_ID symboling CarName fueltype aspiration doornumber carbody drivewheel enginelocation wheelbase carlength carwidth carheight curbweight enginetype cylindernumber enginesize fuelsystem boreratio stroke compressionratio horsepower peakrpm citympg highwaympg price brand model
0 1 3 alfa-romero giulia gas std 2 convertible rwd front 88.6 168.8 64.1 48.8 2548 dohc 4 130 mpfi 3.47 2.68 9.0 111 5000 21 27 13495.0 alfa-romero giulia
1 2 3 alfa-romero stelvio gas std 2 convertible rwd front 88.6 168.8 64.1 48.8 2548 dohc 4 130 mpfi 3.47 2.68 9.0 111 5000 21 27 16500.0 alfa-romero stelvio
2 3 1 alfa-romero Quadrifoglio gas std 2 hatchback rwd front 94.5 171.2 65.5 52.4 2823 ohcv 6 152 mpfi 2.68 3.47 9.0 154 5000 19 26 16500.0 alfa-romero Quadrifoglio
3 4 2 audi 100 ls gas std 4 sedan fwd front 99.8 176.6 66.2 54.3 2337 ohc 4 109 mpfi 3.19 3.40 10.0 102 5500 24 30 13950.0 audi 100 ls
4 5 2 audi 100ls gas std 4 sedan 4wd front 99.4 176.6 66.4 54.3 2824 ohc 5 136 mpfi 3.19 3.40 8.0 115 5500 18 22 17450.0 audi 100ls
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
200 201 -1 volvo 145e (sw) gas std 4 sedan rwd front 109.1 188.8 68.9 55.5 2952 ohc 4 141 mpfi 3.78 3.15 9.5 114 5400 23 28 16845.0 volvo 145e (sw)
201 202 -1 volvo 144ea gas turbo 4 sedan rwd front 109.1 188.8 68.8 55.5 3049 ohc 4 141 mpfi 3.78 3.15 8.7 160 5300 19 25 19045.0 volvo 144ea
202 203 -1 volvo 244dl gas std 4 sedan rwd front 109.1 188.8 68.9 55.5 3012 ohcv 6 173 mpfi 3.58 2.87 8.8 134 5500 18 23 21485.0 volvo 244dl
203 204 -1 volvo 246 diesel turbo 4 sedan rwd front 109.1 188.8 68.9 55.5 3217 ohc 6 145 idi 3.01 3.40 23.0 106 4800 26 27 22470.0 volvo 246
204 205 -1 volvo 264gl gas turbo 4 sedan rwd front 109.1 188.8 68.9 55.5 3062 ohc 4 141 mpfi 3.78 3.15 9.5 114 5400 19 25 22625.0 volvo 264gl

205 rows × 28 columns

Festlegung von kategorischen und numerischen Spalten für ML¶

In [72]:
# Define categorical and numerical columns
categorical_columns = ['fueltype', 'aspiration',  'carbody', 'drivewheel',
                       'enginelocation', 'enginetype', 'fuelsystem', 'brand', 'model']
numerical_columns = ['symboling', 'doornumber','cylindernumber','wheelbase', 'carlength', 'carwidth', 'carheight',
                     'curbweight', 'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower',
                     'peakrpm', 'citympg', 'highwaympg']

print(df["fueltype"].unique(), df["aspiration"].unique(),  df["carbody"].unique(), df["drivewheel"].unique(), df["enginelocation"].unique(), 
      df["enginetype"].unique(), df["fuelsystem"].unique())

print(df["enginesize"].unique())
['gas' 'diesel'] ['std' 'turbo'] ['convertible' 'hatchback' 'sedan' 'wagon' 'hardtop'] ['rwd' 'fwd' '4wd'] ['front' 'rear'] ['dohc' 'ohcv' 'ohc' 'l' 'rotor' 'ohcf' 'dohcv'] ['mpfi' '2bbl' 'mfi' '1bbl' 'spfi' '4bbl' 'idi' 'spdi']
[130 152 109 136 131 108 164 209  61  90  98 122 156  92  79 110 111 119
 258 326  91  70  80 140 134 183 234 308 304  97 103 120 181 151 194 203
 132 121 146 171 161 141 173 145]

4. Modeling (ML-Maschinelles Lernen)¶

Mathematische Grundlage: die Kleinste-Quadrate-Methode

4.1 Der allgemeine lineare Fall¶

  1. model = LinearRegression()
  2. model.fit(X_train, y_train)

4.1.1 Ein eindimensionles Linear-Regression:¶

y= a*x + b; x, y: Messwerte; a: Steigerung, b: bias

MDKQ2.svg.png

4.1.2 N-dimensionles Linear-Regression:¶

die Kleinste-Quadrate-Methode(KQM)¶

r= Xα - y;¶

m x (n+1) ; m: Anzahl der Beobachtungen(oder Anzahl der Zeilen in einer Spalte)

a=(a0, a1, a2,... an), n+1-Dimension, Unbekannte; a0: bias

X: mx(n+1) dimensionale Matrix (Beobachtungen-> Xi,j: i Zeile; j: Spalte

y: m-Messwerte (Preise)

LinearRG.png

image.png

Ziel: die Summe durch KQM zu minimieren

image.png

Die Lösung: np.linalg.inv()

image.png

⚠️ Achtung¶

  1. Bedingung für eindeutige Lösung
    Damit die Matrix $A^T A$ stabil invertierbar ist, muss die Anzahl der Zeilen (Beobachtungen) stets größer sein als die Anzahl der Spalten (Variablen/Features).
    Ist das nicht der Fall, existiert keine eindeutige Lösung für das Gleichungssystem.

  2. Lineare Unabhängigkeit der Spalten
    Werden neue Spalten aus bestehenden Daten erzeugt, dürfen sie nicht in einem proportionalen Verhältnis zu anderen Spalten stehen. Andernfalls sind die Spalten linear abhängig, und die Matrix (A^T A) wird singulär (nicht invertierbar).

    • In diesem Fall liefert np.linalg.inv() keine echte Inverse, sondern bestenfalls eine Pseudo-Inverse (z. B. über np.linalg.pinv()), was mathematisch problematisch ist.
    • Beispiel:
    • $x_1 = a \cdot x$ → ❌ nicht erlaubt (linear abhängig)
    • $x_1 = \sqrt{x}$ → ✅ erlaubt (nicht linear abhängig)

4.2 Verfahren von Encoding:¶

Im Rahmen des Projektes werden bei Encoding für katogorischen Daten zwei Verfahren angewendet: One-Hot und Ordinal Verfahren. das lässt sich am besten mit Beispielen erklären:

📌 Beispiel: Encoding von Kategorien¶

zwei Kategorien:

  • carbody: ["sedan", "hatchback", "wagon", "convertible", "hardtop"]
  • enginetype: ["ohc", "l", "dohc", "dohcv", "ohcf", "ohcv", "rotor"]

Beispieldaten:

carbody enginetype
sedan ohc
hatchback l
wagon dohc
convertible dohcv
hardtop ohcv
sedan rotor

🔹 1. Label Encoding¶

from sklearn.preprocessing import LabelEncoder

  • le_carbody = LabelEncoder()

  • le_enginetype = LabelEncoder()

Jede Kategorie wird durch eine Ganzzahl (sortiert nach der Bezeichnung, z.B. ['Z', 'G', 'B'], nach Sortierung: ['B','G','Z'], B=0, G=1, Z=2 dargestellt.

Beispielausgabe:

  • Carbody classes: ['convertible', 'hardtop', 'hatchback', 'sedan', 'wagon']
  • Enginetype classes: ['dohc', 'dohcv', 'l', 'ohc', 'ohcf', 'ohcv', 'rotor']
carbody enginetype carbody_label enginetype_label
sedan ohc 3 3
hatchback l 2 2
wagon dohc 4 0
convertible dohcv 0 1
hardtop ohcv 1 5
sedan rotor 3 6

🔹 2. One-Hot Encoding¶

Jede Kategorie wird in eine eigene Spalte mit 0/1 umgewandelt. from sklearn.preprocessing import OneHotEncoder

  • ohe = OneHotEncoder(sparse=False)
  • encoded = ohe.fit_transform(df[["carbody", "enginetype"]])

Beispielausgabe:

carbody_convertible carbody_hardtop carbody_hatchback carbody_sedan carbody_wagon enginetype_dohc enginetype_dohcv enginetype_l enginetype_ohc enginetype_ohcf enginetype_ohcv enginetype_rotor
0 0 0 1 0 0 0 0 1 0 0 0
0 0 1 0 0 0 0 1 0 0 0 0
0 0 0 0 1 1 0 0 0 0 0 0
1 0 0 0 0 0 1 0 0 0 0 0
0 1 0 0 0 0 0 0 0 0 1 0

✅ Vergleich: LabelEncoder vs. OneHotEncoder¶

Kriterium Label Encoding One-Hot Encoding
Darstellung Jede Kategorie → eine Zahl Jede Kategorie → eigene Spalte mit 0/1
Speicherbedarf Sehr gering (nur eine Spalte) Höher (eine Spalte pro Kategorie)
Interpretation Problematisch: Modell denkt, Zahlen hätten Reihenfolge Klar & eindeutig, keine künstliche Reihenfolge
Gut für Entscheidungsbäume, Random Forest Lineare Modelle, Neuronale Netze
Schlecht für Lineare Modelle (da künstliche Rangordnung) Viele Kategorien (führt zu sehr breiten Matrizen)

⚠️Achtung: ich benutze in meinem Code: pd.get_dummies(drop_first=True). man muss ganz klar den Unterschied zu OneHotEncoder() verstehen.¶

  • ohe = OneHotEncoder(sparse=False)
  • encoded = ohe.fit_transform(df[["carbody", "enginetype"]])
  • um das gleiche wie pd.get_dummies() zu erreichen: df_encoded = pd.DataFrame(encoded, columns=ohe.get_feature_names_out(["carbody","enginetype"])
  • folgender Vergleich hift:
Aspekt OneHotEncoder (sklearn) pd.get_dummies() (pandas)
Output NumPy-Array oder DataFrame (mit Zusatzschritt) Direkt ein Pandas-DataFrame
Wiederverwendbar ✅ Ja (fit/transform für neue Daten) ❌ Nein (funktioniert nur auf vorhandenen Daten)
Integration in Pipeline ✅ Sehr gut (für ML-Workflows in sklearn) ❌ Nicht vorgesehen
Drop-First Option Standardmäßig ❌
(aber man kann drop="first" setzen)
✅ drop_first=True verfügbar
Einfache Anwendung etwas mehr Code (fit/transform nötig) sehr einfach, 1 Zeile

👉 Fazit:

Label Encoding und Ordinal Encoding¶

  1. Vorteile von Label Encoding (LabelEncoder → nur alphabetisch, keine Ranglogik!)
  • Kompakte Darstellung von Kategorien durch numerische Werte.
  • Einfach und effizient, besonders bei einer geringen Anzahl von Kategorien.
  1. Risiken von Label Encoding
  • Kann problematisch sein, wenn das Modell die Zahlenwerte fälschlicherweise als lineare Rangordnung interpretiert.
  • Besonders kritisch bei vielen Kategorien, da gleiche Abstände in den Zahlen nicht zwingend gleiche inhaltliche Unterschiede widerspiegeln.
  1. Anwendung im aktuellen Datensatz
  • In unserem Datensatz gibt es meist nur 2 bis 8 Kategorien, sodass Label Encoding hier gut geeignet ist und keine gravierende Verzerrung zu erwarten ist.
  1. Empfehlung: Ordinal Encoding bei Rangordnung
  • Falls die Kategorien eine fachlich begründete Reihenfolge besitzen (z. B. Hauptschule < Realschule < Abitur < Bachelor < Master < PhD),
    sollte man den OrdinalEncoder verwenden.
  • Damit kann die Rangordnung explizit angegeben werden, sodass die Kodierung die inhaltliche Bedeutung korrekt widerspiegelt.

One-Hot Encoding: vermeidet dieses Problem, erzeugt aber mehr Spalten und damit größere Datensätze.¶

In der Projektarbeit werden folgende Verfahren untersucht:

  1. Kategorische Spalten benutzt OnehotEncoder + numerische Spalten (standardisiert) → lineares Modell
  2. Kategorische Spalten benutzt LabelEncoder + numerische Spalten (standardisiert) → lineares Modell
  3. Kategorische Spalten benutzt LabelEncoder + numerische Spalten (standardisiert) + Ausreißer behandeln → lineares Modell
  • Aufbereitung von highwaympg: limit_highwaympg_r=52
  • Wenn highwaympg > 52, die Zeile wird als Ausreißer gelöscht. Es gibt zwei Ausreißer
  1. Kategorische Spalten benutzt LabelEncoder + numerische Spalten (standardisiert) + neue Spalten durch $x_1 = \sqrt{x}$, $x_2 = x^2$

Funktion für Modelltraining: training_carmodel(df_cp, categorical_columns, numerical_columns,encoding_mode=2, test_daten_size=0.2, squared_cols=0)

squared_cols: 0, 1, 2; 0: keine neue Spalten;

  • 1: $x_1 = \sqrt{x}$ ; falls $x < 0 \;\Rightarrow\; x_1 = -\sqrt{|x|}$
  • 2: $x_2 = x^2$

was bedeutet "standardisiert"?

StandardScaler() ist ein Werkzeug aus scikit-learn, das numerische Features standardisiert: Es bringt alle angegebenen Spalten (Features) auf eine gemeinsame Skala, indem jeder Wert so transformiert wird:

z = x−μσ

z = σx−μ

Dabei ist x der Originalwert, μ der Mittelwert und σ die Standardabweichung der jeweiligen Spalte.

fit_transform() berechnet zuerst Mittelwert und Standardabweichung für jede numerische Spalte – und transformiert dann alle Werte entsprechend.

Das Ergebnis: Alle Werte der angegebenen numerischen Spalten haben danach Mittelwert=0 und Standardabweichung=1. Die Skalierung ist jetzt vergleichbar, egal ob z.B. Einkommen, Alter oder Wohnfläche vorher unterschiedlich große Zahlen waren.

4.3 Funktionsdefinition für Modelltraining und Vorhersage:¶

4.3.1 training_carmodel() + predict_price()¶

def training_carmodel(df_cp, categorical_columns, numerical_columns,encoding_mode=2, test_daten_size=0.2, squared_cols=0):

def predict_price(df_test, model, label_encoders, scaler, categorical_columns, numerical_columns,squared_cols=0):

df_cp: data Frame¶

categorical_columns = ['fueltype', 'aspiration', 'carbody', 'drivewheel','enginelocation', 'enginetype', 'fuelsystem', 'brand', 'model']¶

numerical_columns = ['symboling', 'doornumber','cylindernumber','wheelbase', 'carlength', 'carwidth', 'carheight','curbweight', 'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower','peakrpm', 'citympg', 'highwaympg']¶

encoding_mode:¶

  • =1: OnehotEncoder;
  • =2: LabelEncoder;

test_daten_size:¶

  • =0.2: 20% der Daten als Testdaten, 80% als Trainingsdaten

squared_cols: 0, 1, 2; 0: keine neue Spalten; für für numerische Spalten¶

  • 1: $x_1 = \sqrt{x}$ ; falls $x < 0 \;\Rightarrow\; x_1 = -\sqrt{|x|}$
  • 2: $x_2 = x^2$

4.3.2 Voraussetzung: categorical_columns und numerical_columns sind definiert¶

  • categorical_columns = ['fueltype', 'aspiration', 'carbody', 'drivewheel', 'enginelocation', 'enginetype', 'fuelsystem', 'brand', 'model']
  • numerical_columns = ['symboling', 'doornumber','cylindernumber','wheelbase', 'carlength', 'carwidth', 'carheight', 'curbweight', 'enginesize', 'boreratio', 'stroke', 'compressionratio', 'horsepower', 'peakrpm', 'citympg', 'highwaympg']

4.3.3 Dokumentation: Scaling & Ordinal-Encoding für ML¶

1️⃣ Numerische Werte: StandardScaler¶

Beim maschinellen Lernen ist es üblich, numerische Features zu standardisieren, damit sie vergleichbare Skalen haben. Dies verbessert die Stabilität und Geschwindigkeit vieler Modelle.

Training¶
scaler = StandardScaler()
df_cp[numerical_columns] = scaler.fit_transform(df_cp[numerical_columns])
  • fit_transform() berechnet Mittelwert (mean) und Standardabweichung (std) und wendet sie an: (x_{scaled} = \frac{x - mean}{std})
  • Die Mittelwerte und Standardabweichungen werden im scaler gespeichert.
Vorhersage / neue Daten¶
df_test[numerical_columns] = scaler.transform(df_test[numerical_columns])
  • Nur transform() wird verwendet, um die gleiche Skalierung wie beim Training zu garantieren.
  • Nie fit_transform() auf neuen Daten verwenden!

2️⃣ Kategoriale Werte: Ordinal-Encoding (LabelEncoder)¶

Training¶
label_encoders = {}
for column in categorical_columns:
    le = LabelEncoder()
    df_cp[column] = le.fit_transform(df_cp[column])
    label_encoders[column] = le
  • Für jede Spalte ein eigener Encoder.
  • fit_transform() lernt die Kategorien und wandelt sie in Zahlen um.
  • Die Encoder werden gespeichert, um bei Vorhersagen dieselbe Kodierung zu nutzen.
Vorhersage / neue Daten¶
if label_encoders is not None:
    for column in categorical_columns:
        df_test[column] = label_encoders[column].transform(df_test[column])
  • Die Spalten werden mit den gespeicherten Encodern transformiert.
  • Neue, unbekannte Kategorien führen zu einem Fehler.

3 ✅ Zusammenfassung¶

Feature-Typ Training Vorhersage Warum wichtig
Numerisch fit_transform() transform() Konsistente Standardisierung
Kategorial (Ordinal) fit_transform() + Encoder speichern transform() mit gespeichertem Encoder Konsistente Kodierung
In [74]:
from sklearn.preprocessing import LabelEncoder, StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score
import pandas as pd
import numpy as np

def training_carmodel(df_cp, categorical_columns, numerical_columns,
                      encoding_mode=2, test_daten_size=0.2, squared_cols=0):
    """
    Trainiert ein lineares Regressionsmodell auf Auto-Daten.

    Parameters:
    -----------
    df_cp : pd.DataFrame
        Trainingsdaten inklusive Zielspalte 'price'.
    categorical_columns : list
        Liste der kategorialen Spalten.
    numerical_columns : list
        Liste der numerischen Spalten.
    encoding_mode : int
        1 = One-Hot Encoding, 2 = Ordinal (Label) Encoding
    test_daten_size : float
        Anteil der Daten für den Testsplit.
    squared_cols : int
        0 = keine Transformation, 1 = sqrt Transformation, 2 = quadrieren

    Returns:
    --------
    model : LinearRegression
        Trainiertes Regressionsmodell
    label_encoders : dict
        Dictionary mit LabelEncodern für jede kategoriale Spalte (nur bei encoding_mode=2)
    scaler : StandardScaler
        Scaler für numerische Spalten
    mse : float
        Mean Squared Error auf Testdaten
    r2_square : float
        R^2 Score auf Testdaten
    pred_df : pd.DataFrame
        Vergleich von tatsächlichen und vorhergesagten Preisen
    """

    # ----------------------
    # Encoding
    # ----------------------
    if encoding_mode == 1:
        df_cp = pd.get_dummies(df_cp, columns=categorical_columns, drop_first=True)
        label_encoders = None
    else:
        label_encoders = {}
        for column in categorical_columns:
            le = LabelEncoder()
            df_cp[column] = le.fit_transform(df_cp[column])
            label_encoders[column] = le

    # ----------------------
    # Feature Engineering
    # ----------------------
    df_cp['power_to_weight_ratio'] = df_cp['horsepower'] / df_cp['curbweight']
    #df_cp['log_enginesize'] = np.log(df_cp['enginesize'] + 1.2)


    for column in numerical_columns:
        if squared_cols == 2:  #x^2
            df_cp[f'{column}_squared'] = df_cp[column] ** 2
        elif squared_cols == 1:  # sqrt(x)
            df_cp[f'{column}_squared'] = np.where(
                df_cp[column] <= 0,
                -np.sqrt(abs(df_cp[column])),
                np.sqrt(abs(df_cp[column]))
            )

    # ----------------------
    # Scaling
    # ----------------------
    scaler = StandardScaler()
    df_cp[numerical_columns] = scaler.fit_transform(df_cp[numerical_columns])

    # ----------------------
    # Train-Test-Split
    # ----------------------
    X = df_cp.drop(['price', 'car_ID', 'CarName'], axis=1)
    y = df_cp['price']
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_daten_size, random_state=42)

   # print("Rows:", len(df_cp), "columns", len(df_cp.columns) )
    
    # ----------------------
    # Modelltraining
    # ----------------------
    model = LinearRegression()
    model.fit(X_train, y_train)

    # ----------------------
    # Vorhersagen
    # ----------------------
    y_pred = model.predict(X_test)

    # ----------------------
    # Modellbewertung
    # ----------------------
    mse = mean_squared_error(y_test, y_pred)
    r2_square = r2_score(y_test, y_pred)

    pred_df = pd.DataFrame({
        'Actual Value': y_test,
        'Predicted Value': y_pred,
        'Difference': y_test - y_pred,
        'rel%': abs((y_test - y_pred)) / y_test * 100
    })

    #print(f"r2_score: {r2_score:.4f}, MSE: {mse:.2f}")

    return model, label_encoders, scaler, mse, r2_square, pred_df


def predict_price(df_test, model, label_encoders, scaler, categorical_columns, numerical_columns,
                  squared_cols=0):
    """
    Macht Vorhersagen für neue Auto-Daten basierend auf einem trainierten Modell.

    Parameters:
    -----------
    df_test : pd.DataFrame
        Neue Daten zum Vorhersagen
    model : LinearRegression
        Trainiertes Modell
    label_encoders : dict
        LabelEncoder pro kategorialer Spalte (nur bei Ordinal-Encoding)
    scaler : StandardScaler
        Scaler für numerische Spalten
    categorical_columns : list
        Liste der kategorialen Spalten
    numerical_columns : list
        Liste der numerischen Spalten
    squared_cols : int
        0 = keine Transformation, 1 = sqrt Transformation, 2 = quadrieren

    Returns:
    --------
    y_pred : np.ndarray
        Vorhergesagte Preise
    """

    # ----------------------
    # Encoding
    # ----------------------
    if label_encoders is not None:
        for column in categorical_columns:
            df_test[column] = label_encoders[column].transform(df_test[column])

    # ----------------------
    # Feature Engineering
    # ----------------------
    df_test['power_to_weight_ratio'] = df_test['horsepower'] / df_test['curbweight']
    #df_test['log_enginesize'] = np.log(df_test['enginesize'] + 1.2)
 

    for column in numerical_columns:
        if squared_cols == 2:
            df_test[f'{column}_squared'] = df_test[column] ** 2
        elif squared_cols == 1:
            df_test[f'{column}_squared'] = np.where(
                df_test[column] <= 0,
                -np.sqrt(abs(df_test[column])),
                np.sqrt(abs(df_test[column]))
            )

    # ----------------------
    # Scaling
    # ----------------------
    df_test[numerical_columns] = scaler.transform(df_test[numerical_columns])

    # ----------------------
    # Features für Prediction vorbereiten
    # ----------------------
    drop_cols = ['car_ID', 'CarName']
    if 'price' in df_test.columns:
        drop_cols.append('price')
    X_new = df_test.drop(drop_cols, axis=1)

    # ----------------------
    # Prediction
    # ----------------------
    y_pred = model.predict(X_new)
    return y_pred

4.3.4 Erklärung von r2_score(y_test, y_pred)¶

R² (Bestimmtheitsmaß) - Erklärung

1️⃣ Definition von R² (Bestimmtheitsmaß)¶

Für ein Test-Set gilt:

R² = 1 - (Σ(y_i - ŷ_i)² / Σ(y_i - ȳ)²)¶

- y_i = echte Werte¶

- ŷ_i = vorhergesagte Werte¶

- ȳ = Mittelwert der echten Werte¶

Interpretation:

  • R² = 1 → perfekte Vorhersage
  • R² = 0 → Modell sagt genauso gut wie Mittelwert
  • R² < 0 → Modell ist schlechter als Mittelwert

2️⃣ Wann wird R² negativ?¶

Σ(y_i - ŷ_i)² > Σ(y_i - ȳ)²¶

  • Das passiert, wenn dein Modell die Werte so schlecht vorhersagt, dass der Mittelwert eine bessere Vorhersage liefern würde.
  • Typische Ursachen:
    • Lineares Modell auf nichtlinearen Zusammenhängen
    • Zu wenige oder falsche Features
    • Ausreißer im Test-Set
    • Übermäßiges Rauschen in den Daten

Negatives R² ist ein Warnsignal, dass dein Modell nicht gut passt. Es bedeutet nicht, dass der Wert falsch berechnet wurde.

4.4 Methodische Untersuchung für das Modelltraining¶

4.4.1 Kategorische Spalten mit One-Hot-Encoding + numerische Spalten (standardisiert) → lineares Modell¶

In [75]:
#One-Hot Encoder
df_cp= df.copy(deep= True)
#training_carmodel(1, test_daten_size=0.2, squared_cols=0)

model, label_encoders, scaler, mse, r2_square, pred_df=  training_carmodel(df_cp, categorical_columns, numerical_columns, 1, test_daten_size=0.2, squared_cols=0)

print(f"r2_score: {r2_square:.4f}, MSE: {mse:.2f}")
print("rel. min.:",pred_df['rel%'].min(), "rel. max.:",pred_df['rel%'].max(), "rel. mean.:",pred_df['rel%'].mean())  

print('\nHistogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage')
fig = px.histogram(pred_df, x='rel%', nbins=30, height=600,
                   title='Histogramm von rel%',
                   labels={'rel%': 'Rel. Preis-Diff in %'})

fig.show()
fig1 = px.scatter(
    pred_df,
    x='Actual Value',   # oder 'y_test', je nachdem wie deine Spalte heißt
    y='rel%', 
    title='Scatterplot: y_test vs. rel%',
    labels={'Actual Value': 'y_test (echte Werte)', 'rel%': 'Relative Abweichung [%]'},
    opacity=0.7
)

fig1.show()
r2_score: 0.7616, MSE: 18819573.48
rel. min.: 0.5607846455711476 rel. max.: 85.11114714159885 rel. mean.: 29.752253801676417

Histogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage
No description has been provided for this image
No description has been provided for this image

4.4.2 Kategorische Spalten mit Ordinal-Encoding + numerische Spalten (standardisiert) → lineares Modell¶

In [76]:
#Ordinal Encoder
#categorical colums as ordinal value + numerical columns (standardisiert) -> Linear model

df_cp= df.copy(deep= True)
# mse, r2_square, model, pred_df= training_carmodel(df_cp,2, test_daten_size=0.2,squared_cols=0)
model, label_encoders, scaler, mse, r2_square, pred_df=  training_carmodel(df_cp, categorical_columns, numerical_columns, 2, test_daten_size=0.2, squared_cols=0)

print(f"r2_score: {r2_square:.4f}, MSE: {mse:.2f}")
print("rel. min.:",pred_df['rel%'].min(), "rel. max.:",pred_df['rel%'].max(), "rel. mean.:",pred_df['rel%'].mean())  

print('\nHistogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage')
fig = px.histogram(pred_df, x='rel%', nbins=30, height=600,
                   title='Histogramm von rel%',
                   labels={'rel%': 'Rel. Preis-Diff in %'})

fig.show()
fig1 = px.scatter(
    pred_df,
    x='Actual Value',   # oder 'y_test', je nachdem wie deine Spalte heißt
    y='rel%', 
    title='Scatterplot: y_test vs. rel%',
    labels={'Actual Value': 'y_test (echte Werte)', 'rel%': 'Relative Abweichung [%]'},
    opacity=0.7
)
fig1.show()
r2_score: 0.8655, MSE: 10614160.91
rel. min.: 0.29296739271161415 rel. max.: 73.32108314015358 rel. mean.: 16.93294809642359

Histogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage
No description has been provided for this image
No description has been provided for this image

Das Ergebnis zeigt dass bei One-Hot Encoding schlechter ist als bei Label-Encoding.¶

Das ist ein sehr kompliziertes Thema. hier hift KI zu erklären.

Meine Frage:¶

In meinen Daten habe ich mehrere kategorische Spalten, wobei jede Spalte eine unterschiedliche Anzahl von Kategorien enthält.

Beispiele:

  • fueltype: ['gas', 'diesel']
  • carbody: ['convertible', 'hardtop', 'hatchback', 'sedan', 'wagon']

Beim Modelltraining habe ich pd.get_dummies(drop_first=True) verwendet und festgestellt, dass die Performance des Modells mit One-Hot-Encoding schlechter ist als mit Label-Encoding.

Meine Frage: Liegt das schlechtere Ergebnis daran, dass

  1. die Anzahl der Kategorien zwischen den verschiedenen Spalten stark variiert,
  2. oder an der Verwendung von drop_first=True,
  3. oder daran, dass die maximale Anzahl der Kategorien relativ klein ist (z. B. ≤10), sodass Label-Encoding, obwohl die Kategorien einfach nach Zahlen sortiert werden, gut funktioniert, da die Unterschiede zwischen den Zahlen nicht zu groß sind (z. B. 1, 2, 3, …, 8 und nicht 1 bis 20)?

kann du meine Formulierung besser organisieren und verbessern

Antwort:¶

Analyse: One-Hot-Encoding vs. Label-Encoding bei unterschiedlichen Kategorien¶

Du hast festgestellt, dass One-Hot-Encoding schlechter performt als Label-Encoding. Hier ist eine systematische Analyse.


1️⃣ Einfluss der unterschiedlichen Anzahl von Kategorien¶

  • Beispiele:

    • fueltype → 2 Kategorien (gas, diesel)
    • carbody → 5 Kategorien (convertible, …, wagon)
    • andere Spalten evtl. bis 10 Kategorien
  • Problem bei One-Hot-Encoding:

    • Jede Kategorie wird in eine eigene Dummy-Spalte umgewandelt.
    • Modelle wie lineare Regression oder neuronale Netze erhalten dadurch sehr unterschiedliche Dimensionen.
    • Spalten mit vielen Dummy-Spalten können dominieren, während kleine Spalten unterrepräsentiert bleiben.
  • Für Baummodelle (DecisionTree, RandomForest) ist das weniger problematisch.


2️⃣ Einfluss von drop_first=True¶

  • drop_first=True entfernt die erste Kategorie jeder Spalte, um Multikollinearität zu vermeiden.
  • Beispiel: carbody mit 5 Kategorien → nur 4 Dummy-Spalten bleiben.
  • Folge: Das Modell muss die Relation zur ausgelassenen Kategorie implizit lernen.
  • Bei kleinen Datensätzen oder spärlicher Verteilung kann das die Leistung verschlechtern.

3️⃣ Einfluss der Anzahl der Kategorien bei Label-Encoding¶

  • Label-Encoding wandelt jede Kategorie in eine Zahl um, z. B.:

    carbody: convertible=0, hardtop=1, hatchback=2, sedan=3, wagon=4

  • Bei kleiner Kategorieanzahl (2–10):

  • Unterschiede zwischen den Zahlen sind klein.

  • Lineare Modelle oder neuronale Netze können die Unterschiede leicht lernen.

  • Label-Encoding ist kompakt und oft stabiler.

  • Bei vielen Kategorien (z. B. 50–100) kann Label-Encoding problematisch sein, weil das Modell eine falsche Rangordnung annimmt.


4️⃣ Zusammenfassung¶

Faktor Effekt auf Modellleistung
One-Hot, unterschiedliche Anzahl Kategorien Spalten mit vielen Dummies dominieren; kleine Spalten werden schwach gewichtet
drop_first=True Kann bei kleinen Datensätzen Informationsverlust erzeugen
Label-Encoding, wenige Kategorien Modell kann Rangordnung leicht lernen; Zahlenunterschied klein → kompakter und stabiler

🔹 Fazit¶

  • Dass Label-Encoding besser performt, liegt an einer Kombination von Faktoren:
  1. Drop_first → kleine Informationsverluste
  2. One-Hot-Matrix wird bei ungleichen Kategoriespalten sehr breit → schwer für Modell zu lernen
  3. Label-Encoding mit wenigen Kategorien → Modell kann Unterschiede gut interpretieren
  • Empfehlung:
  • Lineare Modelle / kleine Datensätze: Label-Encoding ist oft besser.
  • Neuronale Netze oder Random Forests: One-Hot ist sinnvoll, ggf. drop_first=False verwenden und Feature-Skalierung beachten.

4.4.3. Kategorische Spalten mit Ordinal-Encoding + numerische Spalten (standardisiert) + Ausreißer behandeln → lineares Modell¶

Aufbereitung von highwaympg: limit_highwaympg_r=52¶

Wenn highwaympg > 52, die Zeile wird als Ausreißer gelöscht. Es gibt zwei Ausreißer¶

In [77]:
#Ordinal Encoder
df_cp= df.copy(deep= True)

# entfernen die Zeilen wo  df['highwaympg'] >= limit_highwaympg_r] 
df_cp= df_cp.loc[df_cp['highwaympg'] < limit_highwaympg_r]
model, label_encoders, scaler, mse, r2_square, pred_df=  training_carmodel(df_cp, categorical_columns, numerical_columns, 2, test_daten_size=0.2, squared_cols=0)

print(f"r2_score: {r2_square:.4f}, MSE: {mse:.2f}")
print("rel. min.:",pred_df['rel%'].min(), "rel. max.:",pred_df['rel%'].max(), "rel. mean.:",pred_df['rel%'].mean())  

print('\nHistogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage')
fig = px.histogram(pred_df, x='rel%', nbins=30, height=600,
                   title='Histogramm von rel%',
                   labels={'rel%': 'Rel. Preis-Diff in %'})
fig.show()

fig1 = px.scatter(
    pred_df,
    x='Actual Value',   # oder 'y_test', je nachdem wie deine Spalte heißt
    y='rel%', 
    title='Scatterplot: y_test vs. rel%',
    labels={'Actual Value': 'y_test (echte Werte)', 'rel%': 'Relative Abweichung [%]'},
    opacity=0.7
)
fig1.show()
r2_score: 0.8755, MSE: 12798736.05
rel. min.: 1.294706571517844 rel. max.: 58.00247721352542 rel. mean.: 15.414618233592236

Histogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage
No description has been provided for this image
No description has been provided for this image
In [78]:
import joblib

# Speichern
joblib.dump(model, "linear_model_CarPricePred.pkl")
Out[78]:
['linear_model_CarPricePred.pkl']

4.4.4 Kategorische Spalten mit Ordinal-Encoding + numerische Spalten (standardisiert) + Transformation durch sqrt() oder x^2¶

modellieren nicht linearen Anteil von  numerische Spalten durch zusätzliche Spalten
- squared_cols=1: sqrt(x) ; wenn x< 0 , -sqrt(abs(x)
- squared_cols=2: x^2
In [79]:
#Ordinal Encoder
df_cp= df.copy(deep= True)

df_cp= df_cp.loc[df_cp['highwaympg'] < limit_highwaympg_r]
model, label_encoders, scaler, mse, r2_square, pred_df=  training_carmodel(df_cp, categorical_columns, numerical_columns, 2, test_daten_size=0.2, squared_cols=1)

print(f"r2_score: {r2_square:.4f}, MSE: {mse:.2f}")
print("rel. min.:",pred_df['rel%'].min(), "rel. max.:",pred_df['rel%'].max(), "rel. mean.:",pred_df['rel%'].mean())  

print('\nHistogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage')
fig = px.histogram(pred_df, x='rel%', nbins=30, height=600,
                   title='Histogramm von rel%',
                   labels={'rel%': 'Rel. Preis-Diff in %'})
fig.show()
fig1 = px.scatter(
    pred_df,
    x='Actual Value',   # oder 'y_test', je nachdem wie deine Spalte heißt
    y='rel%', 
    title='Scatterplot: y_test vs. rel%',
    labels={'Actual Value': 'y_test (echte Werte)', 'rel%': 'Relative Abweichung [%]'},
    opacity=0.7
        )
fig1.show()
r2_score: 0.8907, MSE: 11235601.92
rel. min.: 0.11879471355248634 rel. max.: 42.03581058286871 rel. mean.: 14.787747555026515

Histogram: Relativer Prozentsatz der Preisdifferenz bei der Voraussage
No description has been provided for this image
No description has been provided for this image
In [80]:
df_cp= df.copy(deep= True)
#df_cp.info()
#df= df.loc[df['highwaympg'] < 52 ]
#df= df.loc[df['highwaympg'] < limit_highwaympg_r] # df is cleaned for 'highwaympg'] < limit_highwaympg_r
#df.info()
#df.cylindernumber.unique()
In [81]:
print("fueltype:",df_cp['fueltype'].unique())
print("fuelsystem:",df_cp['fuelsystem'].unique())
print("carbody:",df_cp['carbody'].unique())
print("drivewheel:",df_cp['drivewheel'].unique())
print("enginelocation:", df_cp['enginelocation'].unique())
print("enginetype:",df_cp['enginetype'].unique())
print("brand:",df_cp['brand'].unique())
print("model:",df_cp['model'].unique())


#categorical_columns = ['fueltype', 'aspiration',  'carbody', 'drivewheel','enginelocation', 'enginetype', 'fuelsystem', 'brand', 'model']
fueltype: ['gas' 'diesel']
fuelsystem: ['mpfi' '2bbl' 'mfi' '1bbl' 'spfi' '4bbl' 'idi' 'spdi']
carbody: ['convertible' 'hatchback' 'sedan' 'wagon' 'hardtop']
drivewheel: ['rwd' 'fwd' '4wd']
enginelocation: ['front' 'rear']
enginetype: ['dohc' 'ohcv' 'ohc' 'l' 'rotor' 'ohcf' 'dohcv']
brand: ['alfa-romero' 'audi' 'bmw' 'chevrolet' 'dodge' 'honda' 'isuzu' 'jaguar'
 'maxda' 'mazda' 'buick' 'mercury' 'mitsubishi' 'Nissan' 'nissan'
 'peugeot' 'plymouth' 'porsche' 'porcshce' 'renault' 'saab' 'subaru'
 'toyota' 'toyouta' 'vokswagen' 'volkswagen' 'vw' 'volvo']
model: ['giulia' 'stelvio' 'Quadrifoglio' '100 ls' '100ls' 'fox' '5000' '4000'
 '5000s (diesel)' '320i' 'x1' 'x3' 'z4' 'x4' 'x5' 'impala' 'monte carlo'
 'vega 2300' 'rampage' 'challenger se' 'd200' 'monaco (sw)' 'colt hardtop'
 'colt (sw)' 'coronet custom' 'dart custom' 'coronet custom (sw)' 'civic'
 'civic cvcc' 'accord cvcc' 'accord lx' 'civic 1500 gl' 'accord'
 'civic 1300' 'prelude' 'civic (auto)' 'MU-X' 'D-Max ' 'D-Max V-Cross'
 'xj' 'xf' 'xk' 'rx3' 'glc deluxe' 'rx2 coupe' 'rx-4' '626' 'glc'
 'rx-7 gs' 'glc 4' 'glc custom l' 'glc custom' 'electra 225 custom'
 'century luxus (sw)' 'century' 'skyhawk' 'opel isuzu deluxe' 'skylark'
 'century special' 'regal sport coupe (turbo)' 'cougar' 'mirage' 'lancer'
 'outlander' 'g4' 'mirage g4' 'montero' 'pajero' 'versa' 'gt-r' 'rogue'
 'latio' 'titan' 'leaf' 'juke' 'note' 'clipper' 'nv200' 'dayz' 'fuga'
 'otti' 'teana' 'kicks' '504' '304' '504 (sw)' '604sl' '505s turbo diesel'
 'fury iii' 'cricket' 'satellite custom (sw)' 'fury gran sedan' 'valiant'
 'duster' 'macan' 'panamera' 'cayenne' 'boxter' '12tl' '5 gtl' '99e'
 '99le' '99gle' None 'dl' 'brz' 'baja' 'r1' 'r2' 'trezia' 'tribeca'
 'corona mark ii' 'corona' 'corolla 1200' 'corona hardtop'
 'corolla 1600 (sw)' 'carina' 'mark ii' 'corolla' 'corolla liftback'
 'celica gt liftback' 'corolla tercel' 'corona liftback' 'starlet'
 'tercel' 'cressida' 'celica gt' 'rabbit' '1131 deluxe sedan' 'model 111'
 'type 3' '411 (sw)' 'super beetle' 'dasher' 'rabbit custom' '145e (sw)'
 '144ea' '244dl' '245' '264gl' 'diesel' '246']

5. Evaluation (Bewertung und Interpretation der Ereignisse)¶

5.1 Modellbewertung¶

Es gibt kategorische und numerische Daten. Für Machine-Learning müssen die Kategorie in Zhal umgewandelt werden.

Es sind hier zwei Methden One-Hot Encoding und Ordinal Encoding angewendt.

Methode R² Score MSE rel. min. rel. max. rel. mean.
One-Hot-Encoding 0.7505 19,693,826.14 1.47 86.35 31.83
Ordinal-Encoding 0.8660 10,580,608.45 0.22 72.77 16.96
Ordinal-Encoding - Ausreißer 0.8771 12,637,217.35 1.77 57.74 15.15
Ordinal-Encoding + Sqrt() 0.8919 11,117,848.75 0.61 41.78 14.71

Verschiedene Mothoden für Encoding werden untersucht.

5.1.1 One-Hot-Encoding:¶

  1. One-Hot-Encoding ist gut, wenn die Kategorien wenige und gleichmäßig verteilt sind
  2. Wenn die Spalten sehr unterschiedlich viele Kategorien haben, kann das Modell eine Verzerrung bekommen.

Die Spalten haben ungleichmäße Anzahl von Kategorien. Der Wert von R² Score=0.7505 zeigt dass One-Hot-Encoding nicht die gute Wahl ist¶

5.1.2 Ordinal-Encoding:¶

  1. Ordinal Encoding ist gut, wenn es eine natürliche Reihenfolge der Kategorien (Ordinalskala) gibt Beispiel:

Schulnoten: A > B > C > D > F ( 1,2,3,4,5)¶

Größe: small < medium < large¶

Zufriedenheit: low < medium < high¶

Dann macht es Sinn, sie als Zahlen darzustellen (small=0, medium=1, large=2), weil die Reihenfolge auch eine Bedeutung hat.¶

  1. Ordinal Encoding erzeugt eine künstliche Ordnung (nach Buchstab sortiert) annehmen, die gar nicht existiert. Das beeinträchtigt die Genauigkeit des Modells

  2. In Fuelsystem z.B. gibt es so viele Werte ['mpfi', '2bbl', 'mfi', '1bbl', 'spfi', '4bbl', 'idi', 'spdi']. Diese Werte sind nach Buchstaben sortiert und eine Zahl 1 bis 8 vergeben. Das ist nicht optimal.

  3. Die Lösung wäre: Die Reihenfolge für ['mpfi', '2bbl', 'mfi', '1bbl', 'spfi', '4bbl', 'idi', 'spdi'] durch Bewertung der Relevanz zu Preis ist zu bestimmen und in eine Zahlenskala umzuwandeln.

    Das ist eine Aufgabe der fachlichen Bewertung und Datenaufbereitung

Das Ergebnis (R² Score=0.86) zeigt dass es auch ohne Reihenfolgebestimmung besseres Ergbnis als One-Hot Encoding erzielt hat¶

5.1.3 Ordinal-Encoding + Behandlung der Ausreißer:¶

Durch statistische Analyse werden Ausreißer beseitigt. Das Ergebnis(R² Score=0.8771) verbessert sich etwas.¶

5.1.4 Ordinal-Encoding + Sqrt():¶

Das Modell wird mit LinearRegression() trainiert.

Dieses Verfahren setzt voraus, dass die Werte in den Features linear mit dem Zielwert (Preis) zusammenhängen – das wäre der Idealfall.

In der Praxis ist das jedoch selten gegeben. Daher ist es üblich, die numerischen Variablen je nach Datenanalyse mit Transformationen wie log(), sqrt() oder x² zu verändern, um Zusammenhänge besser darzustellen.

Das Beispiel mit der sqrt()-Transformation zeigt deutlich, dass sich die Modellgüte verbessert: der R²-Score steigt auf 0.8919.

5.2 Transformation von kategorialen Merkmale in numerische Werte¶

Folgende Merkmale in der Daten:¶

Feature Kategorien
fueltype gas, diesel
fuelsystem mpfi, 2bbl, mfi, 1bbl, spfi, 4bbl, idi, spdi
carbody convertible, hatchback, sedan, wagon, hardtop
drivewheel rwd, fwd, 4wd
enginelocation front, rear
enginetype dohc, ohcv, ohc, l, rotor, ohcf, dohcv
brand alfa-romero, audi, bmw, chevrolet, dodge, honda, isuzu, jaguar, maxda, mazda, buick, mercury, mitsubishi, Nissan, nissan, peugeot, plymouth, porsche, porcshce, renault, saab, subaru, toyota, toyouta, vokswagen, volkswagen, vw, volvo

model: ['giulia' 'stelvio' 'Quadrifoglio' '100 ls' '100ls' 'fox' '5000' '4000' '5000s (diesel)' '320i' 'x1' 'x3' 'z4' 'x4' 'x5' 'impala' 'monte carlo' 'vega 2300' 'rampage' 'challenger se' 'd200' 'monaco (sw)' 'colt hardtop' 'colt (sw)' 'coronet custom' 'dart custom' 'coronet custom (sw)' 'civic' 'civic cvcc' 'accord cvcc' 'accord lx' 'civic 1500 gl' 'accord' 'civic 1300' 'prelude' 'civic (auto)' 'MU-X' 'D-Max ' 'D-Max V-Cross' 'xj' 'xf' 'xk' 'rx3' 'glc deluxe' 'rx2 coupe' 'rx-4' '626' 'glc' 'rx-7 gs' 'glc 4' 'glc custom l' 'glc custom' 'electra 225 custom' 'century luxus (sw)' 'century' 'skyhawk' 'opel isuzu deluxe' 'skylark' 'century special' 'regal sport coupe (turbo)' 'cougar' 'mirage' 'lancer' 'outlander' 'g4' 'mirage g4' 'montero' 'pajero' 'versa' 'gt-r' 'rogue' 'latio' 'titan' 'leaf' 'juke' 'note' 'clipper' 'nv200' 'dayz' 'fuga' 'otti' 'teana' 'kicks' '504' '304' '504 (sw)' '604sl' '505s turbo diesel' 'fury iii' 'cricket' 'satellite custom (sw)' 'fury gran sedan' 'valiant' 'duster' 'macan' 'panamera' 'cayenne' 'boxter' '12tl' '5 gtl' '99e' '99le' '99gle' 'dl' 'brz' 'baja' 'r1' 'r2' 'trezia' 'tribeca' 'corona mark ii' 'corona' 'corolla 1200' 'corona hardtop' 'corolla 1600 (sw)' 'carina' 'mark ii' 'corolla' 'corolla liftback' 'celica gt liftback' 'corolla tercel' 'corona liftback' 'starlet' 'tercel' 'cressida' 'celica gt' 'rabbit' '1131 deluxe sedan' 'model 111' 'type 3' '411 (sw)' 'super beetle' 'dasher' 'rabbit custom' '145e (sw)' '144ea' '244dl' '245' '264gl' 'diesel' '246']

Es ist grundsätzlich möglich, einzelne Features fachlich zu bewerten und numerisch zu kodieren, z. B. gas = 1 und diesel = 1.2.

z.B. die Anzahl von Tür und Zylinder wird bei der Datenbereinigung in Zahlen umgewandelt.

Die Durchführung einer solchen Bewertung setzt jedoch fundierte fachliche Kenntnisse voraus. Im Rahmen dieses Projekts ist dies nicht realistisch umzusetzen, da es den zeitlichen Rahmen deutlich überschreiten würde.

5.3 Modellauswahl für die Anwendung(Deployment)¶

Transformation wie log(), sqrt() oder x² können zwar zu einer verbesserten Modellgüte führen, ihre Wirkung ist jedoch stark von den Daten abhängig.¶

Für eine allgemeinere und praxisgerechte Anwendung habe ich daher das Verfahren Ordinal-Encoding + Ausreißerbehandlung gewählt.¶

6. Deployment (Anwendung der Ergebnisse)¶

6.1 Rahmenbedingungen für das Modelltraining und die Preisvorhersage¶

Das in Kapitel 4.4.3 vorgestellte Verfahren liefert die besten Ergebnisse und wird daher für das Modelltraining eingesetzt:

Kategorische Spalten als Ordinalwerte + numerische Spalten (standardisiert) + Ausreißerbehandlung → Lineares Modell

Die folgenden Überlegungen bilden die Grundlage des Modelltrainings:

  1. Die Daten umfassen verschiedene Autohersteller und -modelle. Die Preise liegen im Bereich von min. 5.118 € bis max. 45.400 €.
  2. Das Unternehmen (Auftraggeber) soll die Möglichkeit haben, ein Modell gezielt für ein bestimmtes Preissegment zu trainieren.
  3. Das Dashboard bietet die Option, ein gewünschtes Preissegment auszuwählen.
  4. Das Modell wird anschließend auf die Daten des ausgewählten Preisbereichs trainiert.
  5. Auf Basis des trainierten Modells kann das Unternehmen durch Eingabe bestimmter Parameter Preisvoraussagen durchführen.

Parameter für das Modelltraining:

encoding_mode: 2 fest

test_daten_size: 0.2 wählbar

squared_cols: 0 fest

Preisbereich: Auswahl durch Anwender wählbar

6.2 Dashborad für Modelltraining¶

Bedingungsanweisung für das Dashboard¶

image.png

Bedingungsanweisung für das Dashboard¶

  1. Initialisierung beim Start

    • Beim Starten des Dashboards wird das Modell mit den gesamten Daten trainiert.
    • Folgende Darstellungen sind sichtbar:
      • Histogramm der Preise
      • Textfeld zur Anzeige der Modellergebnisse
      • Histogramm der relativen Abweichungen (%) für die Testdaten
      • Scatterplot der Preisdifferenzen für einzelne Werte
  2. Auswahl des Preissegments

    • Zwei Slider ermöglichen die Auswahl eines Preisbereichs.
    • Beim Verschieben der Slider wird das Histogramm der ausgewählten Datensätze angezeigt.
    • Das neue Histogramm wird über dem Original-Histogramm dargestellt, sodass der ausgewählte Bereich klar erkennbar ist.
  3. Training mit ausgewählten Datensätzen

    • Durch Drücken des Buttons „Start Training“ wird das Modell mit den ausgewählten Daten (Teil der Gesamtdaten) trainiert.
    • Die Darstellungen (Histogramme, Scatterplot, Textfeld) werden nach dem Training aktualisiert.
  4. Bewertung der Ergebnisse

    • Die Qualität des Modells kann anhand der angezeigten Werte in den Diagrammen und Textfeldern bewertet werden.
In [40]:
#df= df.loc[df['highwaympg'] < limit_highwaympg_r] # df is cleaned for 'highwaympg'] < limit_highwaympg_r
# def training_carmodel(df_cp, categorical_columns, numerical_columns,encoding_mode=2, test_daten_size=0.2, squared_cols=0):

df= df.loc[df['highwaympg'] < 52]
#df.info()
start_price = float(df['price'].min())
end_price = float(df['price'].max())
print(type(start_price))
start_price, end_price
<class 'float'>
Out[40]:
(5118.0, 45400.0)
In [82]:
import dash
from dash import Dash, dcc, html, Input, Output, callback
import plotly.express as px
import plotly.graph_objects as go
import json
import pandas as pd


global model_train, label_encoders_train,scaler_train, mse_train, r2_square_train, df_pred_train
global df_train    # copy of orig. df
global df_filtered # filtered df for ineractive modelltraining

row_count=203
df_train= df.copy(deep= True) # df_train is changed after Training 

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']

app = Dash(__name__, external_stylesheets=external_stylesheets)
styles = {
    'pre': {
        'border': 'thin lightgrey solid',
        'overflowX': 'scroll'
    }
}
slider_value_l=0
slider_value_r=50000
slider_trained_l=0
slider_trained_r=50000

# inital Modelltraining
# entfernen die Zeilen wo  df['highwaympg'] >= limit_highwaympg_r] limit_highwaympg_r=52


model_train, label_encoders_train,scaler_train, mse_train, r2_square_train, df_pred_train =  training_carmodel(df_train, 
                                              categorical_columns, numerical_columns, 2, test_daten_size=0.2, squared_cols=0)

#show the relative deviation of the price : abs(price_diff) / price_orig *100: -> 'rel%': abs((y_test - y_pred)) / y_test * 100
# print("min.:",df_pred_train['rel%'].min(), "max.:",df_pred_train['rel%'].max(), "mean.:",df_pred_train['rel%'].mean())  

my_outstr = (
    f"r2_score: {r2_square_train:.4f},  "
    f"mse: {mse:.4f},  "
    f"min.%: {df_pred_train['rel%'].min():.4f},  "
    f"max.%: {df_pred_train['rel%'].max():.4f},  "
    f"mean%.: {df_pred_train['rel%'].mean():.4f}"
    )
# show original price histogram: go.Histogram() create histogram
# size=300 ist the balkensize, 300 means 300$ for the Bin-width
# height=400 height in pixel, ähnlic wie 'height': '400px' in CSS. aber als string '400px' angegeben.
# for plotly: besser height=400
orig_hist = go.Histogram(
        x=df['price'],
        xbins=dict(
        size=300      # Balkenabstand bzw. Bin-Größe
        ),
        nbinsx=40,
        opacity=0.5,
        name='Original'
    )
fig = go.Figure(orig_hist)

'''
fig.update_layout(
    height=400,   # Höhe in Pixel
    width=800     # optional auch Breite
)
'''
app.layout = html.Div([
    # Erst das Histogramm
    html.Div(
        dcc.Graph(id='compare_histogram', style={ 'width': '100%'}), # 'height':'500px',
        style={'width': '100%', 'padding': '10px'}
    ),

  html.Div(id='result_text', style={'whiteSpace': 'pre-line', 'padding': '10px', 'border': '1px solid #ccc'}),
    # Dann das Slider-DIV darunter
    html.Div([
        #html.Label('Slider Links'),
        dcc.Slider(
            id='price_slider_l',
            min=float(start_price),
            max=float(end_price),
            value=float(start_price),
            step=300.0,
            marks={i: str(i) for i in range(int(start_price), int(end_price)+1, 300)}
        ),

        #html.Label('Slider Rechts'),
        dcc.Slider(
            id='price_slider_r',
            min=float(start_price),
            max=float(end_price),
            value=float(end_price),
            step=300.0,             #300$ a step
            marks={i: str(i) for i in range(int(start_price), int(end_price)+1, 300)}
        )
    ], style={'width': '90%', 'padding': '10px'}),
    
 
    # 2 elements in a DIV: button + histogram
    html.Div([
        html.Div([
             html.Button('Start Training', 
                id='refresh_button', 
                n_clicks=0,
                style={
                        'margin': '10px',
                        'height': '40px',
                        'border': '3px solid black',   # <--- hier wird der Rahmen dicker
                        'borderRadius': '8px'          # optional: abgerundete Ecken
                         }
                       ),

                    dcc.Input(
                        id='output_trained',
                        type='text',
                        #placeholder='10000', 
                        value=my_outstr, 
                        style={'width': '100%'}
                    ),

            
              html.Div(id='alert_message',
              style={
                 'color': 'red',
                 'margin': '10px',
                 'fontWeight': 'bold'
                 
             })
            ], style={'display': 'flex', 'alignItems': 'center'}),

         html.Div([
               dcc.Graph( id='rel_histogram', 
                   figure=go.Figure( data=[go.Histogram(x= df_pred_train['rel%'])],
                   layout=go.Layout(height=400, 
                                    width=700, 
                                    title='Histogram: Die Häufigkeit der rel%-Abweichung',
                                    xaxis_title='Relative Abweichung [%]',
                                    yaxis_title='Häufigkeit'
                                   )
                                 )
                     ),

              dcc.Graph(id='scatter_plot',
                  figure=go.Figure(
                         data=[go.Scatter(
                         x=df_pred_train['Actual Value'],
                         y=df_pred_train['rel%'],
                         mode='markers',
                         opacity=0.7,
                          marker=dict(color='blue')
                         )],
                  layout=go.Layout(
                       height=400,
                       width=400,
                       title='Scatterplot: y_test(Preis in $) vs. rel% Abweichung',
                       xaxis_title='y_test (echte Werte in $)',
                       yaxis_title='Relative Abweichung [%]'
                         )
                        )
                        )


             
                ], style={'display': 'flex', 'alignItems': 'center'} )

        
            ], style={'width': '100%'})

    
      ] , style={'width': '90%', 'margin': 'auto'})


# check if enough rows exist
@app.callback(
    Output('alert_message', 'children'),
    Input('price_slider_l', 'value'),
    Input('price_slider_r', 'value')
)
def validate_inputs(slider_value_l, slider_value_r):
    df_train = df.copy(deep=True)
    
    if slider_value_l > slider_value_r:
       sl_val=slider_value_r
       slider_value_r= slider_value_l
       slider_value_l=  sl_val
        

    df_filtered = df_train[(df_train['price'] >= slider_value_l) & (df_train['price'] <= slider_value_r)]
    row_count = len(df_filtered)

    if row_count < 60:
        return f"⚠️ Achtung: Gefilterte Datenmenge ist zu klein (nur {row_count} Zeilen, benötigt > 60)."

    return ""



@app.callback(
    Output('compare_histogram', 'figure'),
    Output('result_text', 'children'),
    Input('price_slider_l', 'value'),
    Input('price_slider_r', 'value')   
)
def update_compare_histogram(slider_value_l,slider_value_r):
    # Ungefiltertes Histogramm
    # slider_value_l: left value
    # slider_value_r: right value
    import plotly.graph_objects as go
    global df_filtered, row_count

    # why do the copy hier: df_filtered is a part of df_train. they have the same memory area. 
    # wenn f_filtered is changed, then memory of df_train is also changed even if df_train seem to be not be changed
    df_train= df.copy(deep= True)
    if slider_value_l > slider_value_r:
       sl_val=slider_value_r
       slider_value_r= slider_value_l
       slider_value_l=  sl_val
    
    df_filtered = df_train[(df_train['price'] >= slider_value_l) & (df_train['price'] <= slider_value_r)]
    # a0, a1, ...a29: 30 variable to be calculated, row_count should be 60 (48 for training, 12 for testing 
    row_count= len( df_filtered )   
    #print("row_count=", row_count)

    # Gefiltertes Histogramm
    filtered_hist = go.Histogram(
        x=df_filtered['price'],
        xbins=dict(
        size=300      # Balkenabstand bzw. Bin-Größe
        ),       
        opacity=0.7,
        name=f'Gefiltert ab {slider_value_l}'
    )
    # Beide Histogramme in einer Figure
    fig = go.Figure([orig_hist, filtered_hist])
    fig.update_layout(
        barmode='overlay',
        xaxis_title='Price',
        yaxis_title='Count',
        title='Distribution of Price (Vergleich Original & Gefiltert)',
        height=450 #600
    )
    fig.update_xaxes(
        tickmode='linear',
        tick0=0,
        dtick=2000
    )
    mystr = f"price_start: {slider_value_l}, price_end: {slider_value_r}, row_count: {row_count} ( should be > 60)"
    return fig, mystr


@app.callback(
    Output('rel_histogram', 'figure'),
    Output('scatter_plot', 'figure'),
    Output('output_trained', 'value'),
    Input('refresh_button', 'n_clicks')
)
def update_rel_histogram(n_clicks):
    import plotly.graph_objects as go
    global model_train, label_encoders_train,scaler_train, mse_train, r2_square_train, df_pred_train
    global df_filtered
    global slider_trained_l, slider_trained_r

    # start the modeltraining
    if n_clicks and n_clicks > 0 and row_count >=60 :        
       model_train, label_encoders_train,scaler_train, mse_train, r2_square_train, df_pred_train = training_carmodel(
            df_filtered, categorical_columns, numerical_columns, 2, test_daten_size=0.2, squared_cols=0
        )
        # save the price range  for price prediction     
       slider_trained_l= slider_value_l
       slider_trained_r= slider_value_r

    # print("r2_score:",  r2_square_train, "mse:", mse, min.:",df_pred_train['rel%'].min(), "max.:",df_pred_train['rel%'].max(), "mean.:",df_pred_train['rel%'].mean())  

    my_outstr = (
    f"r2_score: {r2_square_train:.4f}, "
    f"mse: {mse:.4f}, "
    f"min.%: {df_pred_train['rel%'].min():.4f}, "
    f"max.%: {df_pred_train['rel%'].max():.4f}, "
    f"mean%.: {df_pred_train['rel%'].mean():.4f}"
    )

    #print(my_outstr)

    rel_hist = go.Histogram(
            x= df_pred_train['rel%'],
            xbins=dict(size=5),
            nbinsx=100,
            opacity=0.5,
            name='relative in %'
        )
    fig = go.Figure(rel_hist)
    #fig.update_layout(title='relative Abweichung')
    fig.layout=go.Layout(height=400, 
                         width=700, 
                         title='Histogram: Die Häufigkeit der rel%-Abweichung',
                         xaxis_title='Relative Abweichung [%]',
                         yaxis_title='Häufigkeit'
                                   )
                                                     

    #--------------------
    scatter_plt = go.Scatter(
                x=df_pred_train['Actual Value'],
                y=df_pred_train['rel%'],
                mode='markers',
                opacity=0.7,
                marker=dict(color='blue')
                )

    fig1 = go.Figure(scatter_plt)
    fig1.update_layout(
                title='Scatterplot: y_test(Preis in $) vs. rel% Abweichung',
                xaxis_title='y_test (echte Werte in $)',
                yaxis_title='Relative Abweichung [%]',
                height=400,
                width=700
                  )

  #---------------------------
    
    return fig, fig1, my_outstr


if __name__ == '__main__':
    app.run(debug=True, port=8050)
In [ ]:
 

6.3 Dashboard für Preisvoraussage(price prediction)¶

Bedingungsanweisung für das Dashboard¶

image.png

1. Textfeld: Preiseingabe¶

  • Der Anwender gibt hier den gewünschten Preis ein.
  • Basierend auf diesem Preis sucht das Dashboard den bestangepassten Datensatz aus den Trainingsdaten.
  • Der gefundene Datensatz wird in den beiden Auto-Parameter-Fenstern angezeigt, sodass der Anwender alle Parameter einsehen kann.
  • Parameter-Zuordnung:
    • horsepower → Fenster „Motorleistung“
    • enginesize → Fenster „Motorgröße“

2. Textfeld: Motorleistung (horsepower)¶

  • Motorleistung kann hier angepasst werden.
  • Dieser Wert wird für die Preisvoraussage als veränderliche Größe verwendet.

3. Textfeld: Motorgröße (enginesize)¶

  • Motorgröße kann hier angepasst werden.
  • Dieser Wert wird für die Preisvoraussage als veränderliche Größe verwendet.

4. Textarea: Auto-Parameter¶

  • Dient als Informationsfenster für den ausgewählten Datensatz.
  • Alle Parameter des Datensatzes werden hier in zwei Bereichen angezeigt.

5. Button: Preis Berechnen¶

  • Berechnet den Preis für die veränderten Parameter horsepower und enginesize.

6. Textfeld: Vorausgesagter Preis¶

  • Zeigt den berechneten Preis für die aktuellen Parameter an.

7. Textfeld: Preis-Differenz¶

  • Zeigt die Differenz zwischen dem vorausgesagten Preis und dem Preis des ausgewählten Datensatzes.
In [83]:
import dash
from dash import dcc, html, Input, Output, State
import dash_bootstrap_components as dbc

# Initialisiere die Dash-App
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

# Initialwerte
initial_value_3 = 0
initial_value_4 = 0
app.layout = dbc.Container([
    html.H1("Dashboard für Preisvoraussage", className="my-4 text-center"),

    dbc.Row([
        # Spalte 1: Inputs
        dbc.Col(dbc.Card([
            dbc.CardHeader("Eingaben"),
            dbc.CardBody([
                html.Div([
                    html.Label("Preiseingeben"),
                    dcc.Input(
                        id='price_input',
                        type='text',
                        placeholder='10000', 
                        value='10000', 
                        style={'width': '100%'}
                    ),
                ], className="mb-3"),

                html.Div([
                    html.Label("Motorleistung"),
                    dcc.Input(
                        id='ps_value',
                        type='number',
                        value=initial_value_3,
                        style={'width': '100%'}
                    ),
                ], className="mb-3"),

                html.Div([
                    html.Label("Motorgröße"),
                    dcc.Input(
                        id='motor_size',
                        type='number',
                        value=initial_value_4,
                        style={'width': '100%'}
                    ),
                ], className="mb-3"),

                dbc.Button("Preis berechnen", id='action-button', n_clicks=0, 
                           color="primary", className="w-100"),
            ])
        ]), width=3),

        # Spalte 2: Parameter-Info: part 1
        dbc.Col(dbc.Card([
            dbc.CardHeader("Auto-Parameter"),
            dbc.CardBody([
                dcc.Textarea(
                    id='parameter_info',
                    readOnly=True,
                    value='',                   
                    style={
                        'width': '100%',
                        'height': '350px',
                        'backgroundColor': '#f0f0f0',
                        'resize': 'vertical'
                    }                    
                ),
            ])
        ]), width=3),

        # Spalte 3: Parameter-Info: part 2
        dbc.Col(dbc.Card([
            dbc.CardHeader("Auto-Parameter"),
            dbc.CardBody([
                dcc.Textarea(
                    id='parameter_info1',
                    readOnly=True,
                    value='',                   
                    style={
                        'width': '100%',
                        'height': '350px',
                        'backgroundColor': '#f0f0f0',
                        'resize': 'vertical'
                    }                    
                ),
            ])
        ]), width=3),

        
        # Spalte 4: Ergebnisse
        dbc.Col(dbc.Card([
            dbc.CardHeader("Ergebnisse"),
            dbc.CardBody([
                html.Div([
                    html.Label("Vorausgesagter Preis"),
                    dcc.Input(
                        id='price_pred',
                        type='text',
                        readOnly=True,
                        value='Warte auf Button-Klick',
                        style={'width': '100%', 'backgroundColor': '#f0f0f0'}
                    ),
                ], className="mb-3"),

                html.Div([
                    html.Label("Preis-Differenz"),
                    dcc.Input(
                        id='price_diff',
                        type='text',
                        readOnly=True,
                        value='Warte auf Button-Klick',
                        style={'width': '100%', 'backgroundColor': '#f0f0f0'}
                    ),
                ]),
            ])
        ]), width=3),
    ]),
], fluid=True)


# Callback für Nr. 1 und Nr. 2
# Wenn Enter in input-1 gedrückt wird, wird der Wert in output-2 aktualisiert.
# price_input  1
# parameter_info 2
# ps_value 3
# motor_size 4
# action-button
# price_pred  5
# price_diff  6

@app.callback(
    Output('parameter_info', 'value'),
    Output('parameter_info1', 'value'),
    Output('ps_value', 'value'),
    Output('motor_size', 'value'),
    Input('price_input', 'n_submit'),
    State('price_input', 'value'),
    #prevent_initial_call=True 
)
def update_para_ps_engine_size(n_submit, price_input_value):
    #slider_trained_l, slider_trained_r
    global item_car_info
    
    if not price_input_value:
        return "Bitte Preis eingeben"
    # use orig df, df_filtered_pred_new is no allowed to change   
    df_filtered_pred_new = df[(df['price'] >= slider_trained_l) & (df['price'] <= slider_trained_r)]
    price_val = float(price_input_value) # string to float
    len_pred_new = len(df_filtered_pred_new)
    # search for item in df_filtered_pred_new
    idx_nearest = None
    price_nearest = None

    # search for the nearst price index
    idx_nearest = (df_filtered_pred_new['price'] - price_val).abs().idxmin()
    
    price_nearest = df.loc[idx_nearest, 'price']
    #print(idx_nearest, price_nearest)
    ps_val= df.loc[idx_nearest, 'horsepower']
    motor_size_val= df.loc[idx_nearest, 'enginesize']
    
    item_info_str = []
    parts_str=[]
    run_idx = 0
    
    if idx_nearest is not None:
        item_car_info = df_filtered_pred_new.loc[idx_nearest] 
        #print("item_car_info: type", type(item_car_info))
        for col, val in item_car_info.items():
          item_info_str = '\n'.join(f"{col}:   {val}" for col, val in item_car_info.items())
        parts_str = item_info_str.split("enginetype", 1)
        parts_str[1]='enginetype'+ parts_str[1]
        
    #item_info_str = '\n'.join(f"{col}: {val}" for col, val in item_info.items())
               

    return parts_str[0], parts_str[1], str(ps_val), str(motor_size_val)

# Callback für den Button und Felder 5 & 6
# Der Button löst eine Aktion aus, die die Werte in Feld 5 und 6 aktualisiert.
@app.callback(
    Output('price_pred', 'value'),
    Output('price_diff', 'value'),
    Input('action-button', 'n_clicks'),
    State('ps_value', 'value'), # Werte von input-3 und input-4 als State übergeben
    State('motor_size', 'value'),
    prevent_initial_call=True
)
def update_fields_on_button_click(n_clicks, value_3, value_4):
    if n_clicks is None:
        # Dies sollte wegen prevent_initial_call=True nicht direkt beim Start passieren
        # Kann aber hilfreich sein, wenn der Button noch nicht geklickt wurde
        return dash.no_update, dash.no_update
    #print("n_clicks", n_clicks)
    # Hier definierst du die Logik für die Aktualisierung der Felder 5 und 6
    # Basierend auf den Werten aus Feld 3 und 4
    try:
        val3 = float(value_3)
        val4 = float(value_4)
    except (ValueError, TypeError):
        return "Ungültiger Wert", "Ungültiger Wert"

    item_car_info['horsepower']  = val3
    item_car_info['enginesize']  = val4
    #print(item_car_info['horsepower'])
    #print(item_car_info['enginesize'])
       
    #model_train, label_encoders_train,scaler_train, mse_train, r2_square_train, df_pred_train = training_carmodel(
    #       df_filtered, categorical_columns, numerical_columns, 2, test_daten_size=0.2, squared_cols=0
    
    df_item_car_info = pd.DataFrame([item_car_info]) # df_item_car_info: as a DataFrame
    price_before= item_car_info['price']
    price_predicted= predict_price(df_item_car_info, model_train, label_encoders_train, scaler_train, 
                                   categorical_columns, numerical_columns,squared_cols=0)
    
    #print("price_pred_str:", price_predicted)
    #print("price_before:", price_before)
    price_pred_dff= price_predicted[0] - price_before
    price_predicted_str= str(int(price_predicted[0]))
    price_pred_dff_str= str(int(price_pred_dff))

  
    return price_predicted_str, price_pred_dff_str


# Run the app
if __name__ == '__main__':
    app.run(debug=True, port=8051)

    

Fazit und Ausblick¶

1. Projektstruktur nach CRISP-DM¶

Die Projektarbeit wurde vollständig nach dem CRISP-DM-Schema durchgeführt. Sowohl deskriptive Analysen als auch explorative Datenanalysen (EDA) wurden systematisch umgesetzt.

2. Erste Datenübersicht¶

Nach dem Einlesen der Daten wurden gängige Methoden wie df.info(), df.describe(), df.nunique(), df.isna().sum() und df.head() angewendet, um einen ersten Überblick über die Daten zu erhalten.

3. Deskriptive statistische Analyse¶

Anschließend wurden die Daten deskriptiv statistisch analysiert, um die Eigenschaften der Daten noch besser zu verstehen.

4. Explorative Datenanalyse (EDA)¶

EDA wurde eingesetzt, um Muster und Zusammenhänge innerhalb der Daten zu erkennen.

5. Datenaufbereitung¶

Nach der statistischen Untersuchung erfolgte die Datenaufbereitung. Da der Datensatz mit 205 Zeilen und 26 Spalten relativ klein ist, war die Datenbereinigung minimal:

  • Zerlegung von Spalten
  • Umwandlung kategorialer Daten in numerische Formate
  • Behandlung von Ausreißern

6. Maschinelles Lernen¶

Der Schwerpunkt der Arbeit liegt eindeutig im Bereich Maschinelles Lernen:

  • Verschiedene Verfahren für Encoding wurden untersucht und ausführlich diskutiert.
  • Lineare Regression wurde für das Modelltraining verwendet.
  • Es wurde getestet, ob nicht-lineare Transformationen numerischer Spalten die Modellleistung verbessern.
    • Überraschenderweise erzielte die sqrt(x)-Transformation die besten Ergebnisse.
  • Es wurde gezeigt, dass Label-Encoding bessere Ergebnisse liefert als One-Hot-Encoding, was im Dokument detailliert begründet wird.
  • Durch die Behandlung von Ausreißern verbesserte sich die Modellgenauigkeit um etwa 1%.
  • Insgesamt erreicht das Modell einen Score von 90 % (1 = perfekt). Für die Preisvorhersage wurde Label-Encoding verwendet.

7. Dashboard: Modelltraining¶

Am Ende wurden zwei Dashboards erstellt, die funktional und optisch ansprechend sind:

  • Über das Dashboard für das Modelltraining kann ein beliebiges Preissegment ausgewählt und trainiert werden.
  • Einschränkung: Der Preisbereich muss ausreichend groß sein, damit genügend Daten für ein sinnvolles Modelltraining vorhanden sind.

8. Dashboard: Preisvorhersage¶

  • Nach dem Training kann die Preisvorhersage direkt über das Dashboard durchgeführt werden.
  • Ein Referenzpreis wird eingegeben, und über zwei Parameter können Varianten des Autos verändert werden.
  • Das Modell prognostiziert daraufhin den Preis für die veränderte Variante.
  • Tests zeigen, dass das Verfahren auch mit nur 205 Datenzeilen gut funktioniert.
  • Mit größeren Datensätzen könnte die Modellqualität weiter verbessert werden.

9. Ausblick¶

  • Die Arbeit zeigt, dass KI ein wertvolles Werkzeug ist, das die Analyse und Modellierung wesentlich unterstützt.
  • Zukünftig könnten weitere Features, größere Datensätze oder komplexere Modelle die Vorhersagegenauigkeit weiter erhöhen.